001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017-2018 microBean. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.helm.chart.repository; 018 019import java.io.BufferedInputStream; 020import java.io.ByteArrayOutputStream; 021import java.io.InputStream; 022import java.io.IOException; 023 024import java.net.Proxy; 025import java.net.URI; 026import java.net.URISyntaxException; 027import java.net.URL; 028import java.net.URLConnection; 029 030import java.nio.ByteBuffer; 031 032import java.nio.file.CopyOption; 033import java.nio.file.LinkOption; // for javadoc only 034import java.nio.file.StandardCopyOption; 035import java.nio.file.Files; 036import java.nio.file.Path; 037import java.nio.file.Paths; 038 039import java.nio.file.attribute.FileAttribute; // for javadoc only 040 041import java.security.MessageDigest; 042import java.security.NoSuchAlgorithmException; 043 044import java.util.Collection; 045import java.util.Collections; 046import java.util.Iterator; 047import java.util.Map; 048import java.util.Objects; 049import java.util.LinkedHashSet; 050import java.util.Set; 051import java.util.SortedSet; 052import java.util.SortedMap; 053import java.util.TreeSet; 054import java.util.TreeMap; 055 056import java.util.zip.GZIPInputStream; 057 058import javax.xml.bind.DatatypeConverter; 059 060import com.github.zafarkhaja.semver.ParseException; 061import com.github.zafarkhaja.semver.Version; 062 063import hapi.chart.ChartOuterClass.Chart; 064import hapi.chart.MetadataOuterClass.Metadata; 065import hapi.chart.MetadataOuterClass.MetadataOrBuilder; 066 067import org.kamranzafar.jtar.TarInputStream; 068 069import org.microbean.development.annotation.Experimental; 070import org.microbean.development.annotation.Issue; 071 072import org.microbean.helm.chart.Metadatas; 073import org.microbean.helm.chart.StringResolver; 074import org.microbean.helm.chart.TapeArchiveChartLoader; 075 076import org.microbean.helm.chart.resolver.AbstractChartResolver; 077import org.microbean.helm.chart.resolver.ChartResolverException; 078 079import org.yaml.snakeyaml.DumperOptions; 080import org.yaml.snakeyaml.Yaml; 081 082import org.yaml.snakeyaml.constructor.SafeConstructor; 083 084import org.yaml.snakeyaml.nodes.NodeId; 085import org.yaml.snakeyaml.nodes.Tag; 086 087import org.yaml.snakeyaml.representer.Representer; 088 089import org.yaml.snakeyaml.resolver.Resolver; 090 091/** 092 * An {@link AbstractChartResolver} that {@linkplain #resolve(String, 093 * String) resolves} <a 094 * href="https://docs.helm.sh/developing_charts/#charts">Helm 095 * charts</a> from <a 096 * href="https://docs.helm.sh/developing_charts/#create-a-chart-repository">a 097 * given Helm chart repository</a>. 098 * 099 * @author <a href="https://about.me/lairdnelson" 100 * target="_parent">Laird Nelson</a> 101 * 102 * @see #resolve(String, String) 103 */ 104@Experimental 105public class ChartRepository extends AbstractChartResolver { 106 107 108 /* 109 * Static fields. 110 */ 111 112 113 /** 114 * A {@link HelmHome} instance representing the directory tree where 115 * Helm stores its local information. 116 * 117 * <p>This field is never {@code null}.</p> 118 */ 119 private static final HelmHome helmHome = new HelmHome(); 120 121 122 /* 123 * Instance fields. 124 */ 125 126 127 /** 128 * An {@linkplain Path#isAbsolute() absolute} {@link Path} 129 * representing a directory where Helm chart archives may be stored. 130 * 131 * <p>This field will never be {@code null}.</p> 132 */ 133 private final Path archiveCacheDirectory; 134 135 /** 136 * An {@linkplain Path#isAbsolute() absolute} or relative {@link 137 * Path} representing a local copy of a chart repository's <a 138 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 139 * index.yaml}</a> file. 140 * 141 * <p>If the value of this field is a relative {@link Path}, then it 142 * will be considered to be relative to the value of the {@link 143 * #indexCacheDirectory} field.</p> 144 * 145 * <p>This field will never be {@code null}.</p> 146 * 147 * @see #getCachedIndexPath() 148 */ 149 private final Path cachedIndexPath; 150 151 /** 152 * The {@link Index} object representing the chart repository index 153 * as described canonically by its <a 154 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 155 * index.yaml}</a> file. 156 * 157 * <p>This field may be {@code null}.</p> 158 * 159 * @see #getIndex() 160 * 161 * @see #downloadIndex() 162 */ 163 private transient Index index; 164 165 /** 166 * An {@linkplain Path#isAbsolute() absolute} {@link Path} 167 * representing a directory that the value of the {@link 168 * #cachedIndexPath} field will be considered to be relative to. 169 * 170 * <p>This field may be {@code null}, in which case it is guaranteed 171 * that the {@link #cachedIndexPath} field's value is {@linkplain 172 * Path#isAbsolute() absolute}.</p> 173 */ 174 private final Path indexCacheDirectory; 175 176 /** 177 * The name of this {@link ChartRepository}. 178 * 179 * <p>This field is never {@code null}.</p> 180 * 181 * @see #getName() 182 */ 183 private final String name; 184 185 /** 186 * The {@link URI} representing the root of the chart repository 187 * represented by this {@link ChartRepository}. 188 * 189 * <p>This field is never {@code null}.</p> 190 * 191 * @see #getUri() 192 */ 193 private final URI uri; 194 195 /** 196 * The {@link Proxy} representing the proxy server used to establish 197 * a connection to the chart repository represented by this {@link 198 * ChartRepository}. 199 * 200 * <p>This field is never {@code null}.</p> 201 * 202 * @see #ChartRepository(String, URI, Path, Path, Path, boolean, 203 * Proxy) 204 */ 205 private final Proxy proxy; 206 207 208 /* 209 * Constructors. 210 */ 211 212 213 /** 214 * Creates a new {@link ChartRepository} whose {@linkplain 215 * #getCachedIndexPath() cached index path} will be a {@link Path} 216 * relative to the absolute directory represented by the value of 217 * the {@code helm.home} system property, or the value of the {@code 218 * HELM_HOME} environment variable, and bearing a name consisting of 219 * the supplied {@code name} suffixed with {@code -index.yaml}. 220 * 221 * @param name the name of this {@link ChartRepository}; must not be 222 * {@code null} 223 * 224 * @param uri the {@linkplain URI#isAbsolute() absolute} {@link URI} 225 * to the root of this {@link ChartRepository}; must not be {@code 226 * null} 227 * 228 * @exception NullPointerException if either {@code name} or {@code 229 * uri} is {@code null} 230 * 231 * @exception IllegalArgumentException if {@code uri} is {@linkplain 232 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 233 * home" directory 234 * 235 * @see #ChartRepository(String, URI, Path, Path, Path, boolean, 236 * Proxy) 237 * 238 * @see #getName() 239 * 240 * @see #getUri() 241 * 242 * @see #getCachedIndexPath() 243 */ 244 public ChartRepository(final String name, final URI uri) { 245 this(name, uri, null, null, null, false, Proxy.NO_PROXY); 246 } 247 248 /** 249 * Creates a new {@link ChartRepository} whose {@linkplain 250 * #getCachedIndexPath() cached index path} will be a {@link Path} 251 * relative to the absolute directory represented by the value of 252 * the {@code helm.home} system property, or the value of the {@code 253 * HELM_HOME} environment variable, and bearing a name consisting of 254 * the supplied {@code name} suffixed with {@code -index.yaml}. 255 * 256 * @param name the name of this {@link ChartRepository}; must not be 257 * {@code null} 258 * 259 * @param uri the {@linkplain URI#isAbsolute() absolute} {@link URI} 260 * to the root of this {@link ChartRepository}; must not be {@code 261 * null} 262 * 263 * @param reifyHelmHomeIfNecessary if {@code true} and, for whatever 264 * reason, the local Helm home directory structure needs to be 265 * partially or entirely created, then this constructor will attempt 266 * to reify it 267 * 268 * @exception NullPointerException if either {@code name} or {@code 269 * uri} is {@code null} 270 * 271 * @exception IllegalArgumentException if {@code uri} is {@linkplain 272 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 273 * home" directory 274 * 275 * @see #ChartRepository(String, URI, Path, Path, Path, boolean, 276 * Proxy) 277 * 278 * @see #getName() 279 * 280 * @see #getUri() 281 * 282 * @see #getCachedIndexPath() 283 */ 284 public ChartRepository(final String name, final URI uri, final boolean reifyHelmHomeIfNecessary) { 285 this(name, uri, null, null, null, reifyHelmHomeIfNecessary, Proxy.NO_PROXY); 286 } 287 288 /** 289 * Creates a new {@link ChartRepository}. 290 * 291 * @param name the name of this {@link ChartRepository}; must not be 292 * {@code null} 293 * 294 * @param uri the {@link URI} to the root of this {@link 295 * ChartRepository}; must not be {@code null} 296 * 297 * @param cachedIndexPath a {@link Path} naming the file that will 298 * store a copy of the chart repository's {@code index.yaml} file; 299 * if {@code null} then a {@link Path} relative to the absolute 300 * directory represented by the value of the {@code helm.home} 301 * system property, or the value of the {@code HELM_HOME} 302 * environment variable, and bearing a name consisting of the 303 * supplied {@code name} suffixed with {@code -index.yaml} will be 304 * used instead 305 * 306 * @exception NullPointerException if either {@code name} or {@code 307 * uri} is {@code null} 308 * 309 * @exception IllegalArgumentException if {@code uri} is {@linkplain 310 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 311 * home" directory 312 * 313 * @see #ChartRepository(String, URI, Path, Path, Path, boolean, 314 * Proxy) 315 * 316 * @see #getName() 317 * 318 * @see #getUri() 319 * 320 * @see #getCachedIndexPath() 321 */ 322 public ChartRepository(final String name, final URI uri, final Path cachedIndexPath) { 323 this(name, uri, null, null, cachedIndexPath, Proxy.NO_PROXY); 324 } 325 326 /** 327 * Creates a new {@link ChartRepository}. 328 * 329 * @param name the name of this {@link ChartRepository}; must not be 330 * {@code null} 331 * 332 * @param uri the {@link URI} to the root of this {@link 333 * ChartRepository}; must not be {@code null} 334 * 335 * @param cachedIndexPath a {@link Path} naming the file that will 336 * store a copy of the chart repository's {@code index.yaml} file; 337 * if {@code null} then a {@link Path} relative to the absolute 338 * directory represented by the value of the {@code helm.home} 339 * system property, or the value of the {@code HELM_HOME} 340 * environment variable, and bearing a name consisting of the 341 * supplied {@code name} suffixed with {@code -index.yaml} will be 342 * used instead 343 * 344 * @param reifyHelmHomeIfNecessary if {@code true} and, for whatever 345 * reason, the local Helm home directory structure needs to be 346 * partially or entirely created, then this constructor will attempt 347 * to reify it 348 * 349 * @exception NullPointerException if either {@code name} or {@code 350 * uri} is {@code null} 351 * 352 * @exception IllegalArgumentException if {@code uri} is {@linkplain 353 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 354 * home" directory 355 * 356 * @see #ChartRepository(String, URI, Path, Path, Path, boolean, 357 * Proxy) 358 * 359 * @see #getName() 360 * 361 * @see #getUri() 362 * 363 * @see #getCachedIndexPath() 364 */ 365 public ChartRepository(final String name, 366 final URI uri, 367 final Path cachedIndexPath, 368 final boolean reifyHelmHomeIfNecessary) { 369 this(name, uri, null, null, cachedIndexPath, reifyHelmHomeIfNecessary, Proxy.NO_PROXY); 370 } 371 372 /** 373 * Creates a new {@link ChartRepository}. 374 * 375 * @param name the name of this {@link ChartRepository}; must not be 376 * {@code null} 377 * 378 * @param uri the {@link URI} to the root of this {@link 379 * ChartRepository}; must not be {@code null} 380 * 381 * @param archiveCacheDirectory an {@linkplain Path#isAbsolute() 382 * absolute} {@link Path} representing a directory where Helm chart 383 * archives may be stored; if {@code null} then a {@link Path} 384 * beginning with the absolute directory represented by the value of 385 * the {@code helm.home} system property, or the value of the {@code 386 * HELM_HOME} environment variable, appended with {@code 387 * cache/archive} will be used instead 388 * 389 * @param indexCacheDirectory an {@linkplain Path#isAbsolute() 390 * absolute} {@link Path} representing a directory that the supplied 391 * {@code cachedIndexPath} parameter value will be considered to be 392 * relative to; <strong>will be ignored and hence may be {@code 393 * null}</strong> if the supplied {@code cachedIndexPath} parameter 394 * value {@linkplain Path#isAbsolute() is absolute} 395 * 396 * @param cachedIndexPath a {@link Path} naming the file that will 397 * store a copy of the chart repository's {@code index.yaml} file; 398 * if {@code null} then a {@link Path} relative to the absolute 399 * directory represented by the value of the {@code helm.home} 400 * system property, or the value of the {@code HELM_HOME} 401 * environment variable, and bearing a name consisting of the 402 * supplied {@code name} suffixed with {@code -index.yaml} will be 403 * used instead 404 * 405 * @exception NullPointerException if either {@code name} or {@code 406 * uri} is {@code null} 407 * 408 * @exception IllegalArgumentException if {@code uri} is {@linkplain 409 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 410 * home" directory, or if {@code archiveCacheDirectory} is 411 * non-{@code null} and either empty or not {@linkplain 412 * Path#isAbsolute()} 413 * 414 * @see #ChartRepository(String, URI, Path, Path, Path, boolean, 415 * Proxy) 416 * 417 * @see #getName() 418 * 419 * @see #getUri() 420 * 421 * @see #getCachedIndexPath() 422 */ 423 public ChartRepository(final String name, 424 final URI uri, 425 final Path archiveCacheDirectory, 426 final Path indexCacheDirectory, 427 final Path cachedIndexPath) { 428 this(name, uri, archiveCacheDirectory, indexCacheDirectory, cachedIndexPath, false, Proxy.NO_PROXY); 429 } 430 431 /** 432 * Creates a new {@link ChartRepository}. 433 * 434 * @param name the name of this {@link ChartRepository}; must not be 435 * {@code null} 436 * 437 * @param uri the {@link URI} to the root of this {@link 438 * ChartRepository}; must not be {@code null} 439 * 440 * @param archiveCacheDirectory an {@linkplain Path#isAbsolute() 441 * absolute} {@link Path} representing a directory where Helm chart 442 * archives may be stored; if {@code null} then a {@link Path} 443 * beginning with the absolute directory represented by the value of 444 * the {@code helm.home} system property, or the value of the {@code 445 * HELM_HOME} environment variable, appended with {@code 446 * cache/archive} will be used instead 447 * 448 * @param indexCacheDirectory an {@linkplain Path#isAbsolute() 449 * absolute} {@link Path} representing a directory that the supplied 450 * {@code cachedIndexPath} parameter value will be considered to be 451 * relative to; <strong>will be ignored and hence may be {@code 452 * null}</strong> if the supplied {@code cachedIndexPath} parameter 453 * value {@linkplain Path#isAbsolute() is absolute} 454 * 455 * @param cachedIndexPath a {@link Path} naming the file that will 456 * store a copy of the chart repository's {@code index.yaml} file; 457 * if {@code null} then a {@link Path} relative to the absolute 458 * directory represented by the value of the {@code helm.home} 459 * system property, or the value of the {@code HELM_HOME} 460 * environment variable, and bearing a name consisting of the 461 * supplied {@code name} suffixed with {@code -index.yaml} will be 462 * used instead 463 * 464 * @param reifyHelmHomeIfNecessary if {@code true} and, for whatever 465 * reason, the local Helm home directory structure needs to be 466 * partially or entirely created, then this constructor will attempt 467 * to reify it 468 * 469 * @exception NullPointerException if either {@code name} or {@code 470 * uri} is {@code null} 471 * 472 * @exception IllegalArgumentException if {@code uri} is {@linkplain 473 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 474 * home" directory and/or it could not be reified, or if {@code 475 * archiveCacheDirectory} is non-{@code null} and either empty or 476 * not {@linkplain Path#isAbsolute()} 477 * 478 * @see #ChartRepository(String, URI, Path, Path, Path, boolean, 479 * Proxy) 480 * 481 * @see #getName() 482 * 483 * @see #getUri() 484 * 485 * @see #getCachedIndexPath() 486 */ 487 public ChartRepository(final String name, 488 final URI uri, 489 final Path archiveCacheDirectory, 490 final Path indexCacheDirectory, 491 final Path cachedIndexPath, 492 final boolean reifyHelmHomeIfNecessary) { 493 this(name, uri, archiveCacheDirectory, indexCacheDirectory, cachedIndexPath, false, Proxy.NO_PROXY); 494 } 495 496 /** 497 * Creates a new {@link ChartRepository}. 498 * 499 * @param name the name of this {@link ChartRepository}; must not be 500 * {@code null} 501 * 502 * @param uri the {@link URI} to the root of this {@link 503 * ChartRepository}; must not be {@code null} 504 * 505 * @param archiveCacheDirectory an {@linkplain Path#isAbsolute() 506 * absolute} {@link Path} representing a directory where Helm chart 507 * archives may be stored; if {@code null} then a {@link Path} 508 * beginning with the absolute directory represented by the value of 509 * the {@code helm.home} system property, or the value of the {@code 510 * HELM_HOME} environment variable, appended with {@code 511 * cache/archive} will be used instead 512 * 513 * @param indexCacheDirectory an {@linkplain Path#isAbsolute() 514 * absolute} {@link Path} representing a directory that the supplied 515 * {@code cachedIndexPath} parameter value will be considered to be 516 * relative to; will be ignored and hence may be {@code null} if the 517 * supplied {@code cachedIndexPath} parameter value {@linkplain 518 * Path#isAbsolute()} 519 * 520 * @param cachedIndexPath a {@link Path} naming the file that will 521 * store a copy of the chart repository's {@code index.yaml} file; 522 * if {@code null} then a {@link Path} relative to the absolute 523 * directory represented by the value of the {@code helm.home} 524 * system property, or the value of the {@code HELM_HOME} 525 * environment variable, and bearing a name consisting of the 526 * supplied {@code name} suffixed with {@code -index.yaml} will be 527 * used instead 528 * 529 * @param proxy a {@link Proxy} representing a proxy server used to 530 * establish a connection to the chart repository represented by 531 * this {@link ChartRepository}; may be {@code null} in which case 532 * {@link Proxy#NO_PROXY} will be used instead 533 * 534 * @exception NullPointerException if either {@code name} or {@code 535 * uri} is {@code null} 536 * 537 * @exception IllegalArgumentException if {@code uri} is {@linkplain 538 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 539 * home" directory 540 * 541 * @see #getName() 542 * 543 * @see #getUri() 544 * 545 * @see #getCachedIndexPath() 546 */ 547 public ChartRepository(final String name, 548 final URI uri, 549 final Path archiveCacheDirectory, 550 final Path indexCacheDirectory, 551 final Path cachedIndexPath, 552 final Proxy proxy) { 553 this(name, uri, archiveCacheDirectory, indexCacheDirectory, cachedIndexPath, false, proxy); 554 } 555 556 /** 557 * Creates a new {@link ChartRepository}. 558 * 559 * @param name the name of this {@link ChartRepository}; must not be 560 * {@code null} 561 * 562 * @param uri the {@link URI} to the root of this {@link 563 * ChartRepository}; must not be {@code null} 564 * 565 * @param archiveCacheDirectory an {@linkplain Path#isAbsolute() 566 * absolute} {@link Path} representing a directory where Helm chart 567 * archives may be stored; if {@code null} then a {@link Path} 568 * beginning with the absolute directory represented by the value of 569 * the {@code helm.home} system property, or the value of the {@code 570 * HELM_HOME} environment variable, appended with {@code 571 * cache/archive} will be used instead 572 * 573 * @param indexCacheDirectory an {@linkplain Path#isAbsolute() 574 * absolute} {@link Path} representing a directory that the supplied 575 * {@code cachedIndexPath} parameter value will be considered to be 576 * relative to; will be ignored and hence may be {@code null} if the 577 * supplied {@code cachedIndexPath} parameter value {@linkplain 578 * Path#isAbsolute()} 579 * 580 * @param cachedIndexPath a {@link Path} naming the file that will 581 * store a copy of the chart repository's {@code index.yaml} file; 582 * if {@code null} then a {@link Path} relative to the absolute 583 * directory represented by the value of the {@code helm.home} 584 * system property, or the value of the {@code HELM_HOME} 585 * environment variable, and bearing a name consisting of the 586 * supplied {@code name} suffixed with {@code -index.yaml} will be 587 * used instead 588 * 589 * @param reifyHelmHomeIfNecessary if {@code true} and, for whatever 590 * reason, the local Helm home directory structure needs to be 591 * partially or entirely created, then this constructor will attempt 592 * to reify it 593 * 594 * @param proxy a {@link Proxy} representing a proxy server used to 595 * establish a connection to the chart repository represented by 596 * this {@link ChartRepository}; may be {@code null} in which case 597 * {@link Proxy#NO_PROXY} will be used instead 598 * 599 * @exception NullPointerException if either {@code name} or {@code 600 * uri} is {@code null} 601 * 602 * @exception IllegalArgumentException if {@code uri} is {@linkplain 603 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 604 * home" directory and/or it could not be reified 605 * 606 * @see #getName() 607 * 608 * @see #getUri() 609 * 610 * @see #getCachedIndexPath() 611 */ 612 public ChartRepository(final String name, 613 final URI uri, 614 final Path archiveCacheDirectory, 615 Path indexCacheDirectory, 616 Path cachedIndexPath, 617 final boolean reifyHelmHomeIfNecessary, 618 final Proxy proxy) { 619 super(); 620 Objects.requireNonNull(name); 621 Objects.requireNonNull(uri); 622 if (!uri.isAbsolute()) { 623 throw new IllegalArgumentException("!uri.isAbsolute(): " + uri); 624 } 625 626 boolean reified = false; 627 Path helmHomePath = null; 628 629 if (archiveCacheDirectory == null) { 630 helmHomePath = helmHome.toPath(); 631 assert helmHomePath != null; 632 this.archiveCacheDirectory = helmHomePath.resolve("cache/archive"); 633 assert this.archiveCacheDirectory != null; 634 assert this.archiveCacheDirectory.isAbsolute(); 635 if (reifyHelmHomeIfNecessary) { 636 try { 637 helmHome.reify(); 638 reified = true; 639 } catch (final IOException ioException) { 640 throw new IllegalStateException(ioException.getMessage(), ioException); 641 } 642 } 643 } else if (archiveCacheDirectory.toString().isEmpty()) { 644 throw new IllegalArgumentException("archiveCacheDirectory.toString().isEmpty(): " + archiveCacheDirectory); 645 } else if (!archiveCacheDirectory.isAbsolute()) { 646 throw new IllegalArgumentException("!archiveCacheDirectory.isAbsolute(): " + archiveCacheDirectory); 647 } else { 648 this.archiveCacheDirectory = archiveCacheDirectory; 649 } 650 assert this.archiveCacheDirectory != null; 651 assert this.archiveCacheDirectory.isAbsolute(); 652 if (!Files.isDirectory(this.archiveCacheDirectory)) { 653 throw new IllegalArgumentException("!Files.isDirectory(this.archiveCacheDirectory): " + this.archiveCacheDirectory); 654 } 655 656 if (cachedIndexPath == null || cachedIndexPath.toString().isEmpty()) { 657 cachedIndexPath = Paths.get(new StringBuilder(name).append("-index.yaml").toString()); 658 } 659 assert cachedIndexPath != null; 660 661 if (cachedIndexPath.isAbsolute()) { 662 this.indexCacheDirectory = null; 663 this.cachedIndexPath = cachedIndexPath; 664 } else { 665 if (indexCacheDirectory == null) { 666 if (helmHomePath == null) { 667 helmHomePath = helmHome.toPath(); 668 assert helmHomePath != null; 669 } 670 this.indexCacheDirectory = helmHomePath.resolve("repository/cache"); 671 assert this.indexCacheDirectory.isAbsolute(); 672 if (!reified && reifyHelmHomeIfNecessary) { 673 try { 674 helmHome.reify(); 675 reified = true; 676 } catch (final IOException ioException) { 677 throw new IllegalStateException(ioException.getMessage(), ioException); 678 } 679 } 680 } else if (!indexCacheDirectory.isAbsolute()) { 681 throw new IllegalArgumentException("!indexCacheDirectory.isAbsolute(): " + indexCacheDirectory); 682 } else { 683 this.indexCacheDirectory = indexCacheDirectory; 684 assert this.indexCacheDirectory.isAbsolute(); 685 } 686 if (!Files.isDirectory(this.indexCacheDirectory)) { 687 throw new IllegalArgumentException("!Files.isDirectory(this.indexCacheDirectory): " + this.indexCacheDirectory); 688 } 689 this.cachedIndexPath = this.indexCacheDirectory.resolve(cachedIndexPath); 690 } 691 assert this.cachedIndexPath != null; 692 assert this.cachedIndexPath.isAbsolute(); 693 694 this.name = name; 695 this.uri = uri; 696 this.proxy = proxy == null ? Proxy.NO_PROXY : proxy; 697 } 698 699 700 /* 701 * Instance methods. 702 */ 703 704 705 /** 706 * Returns the name of this {@link ChartRepository}. 707 * 708 * <p>This method never returns {@code null}.</p> 709 * 710 * @return the non-{@code null} name of this {@link ChartRepository} 711 */ 712 public final String getName() { 713 return this.name; 714 } 715 716 /** 717 * Returns the {@link URI} of the root of this {@link 718 * ChartRepository}. 719 * 720 * <p>This method never returns {@code null}.</p> 721 * 722 * <p>The {@link URI} returned by this method is guaranteed to be 723 * {@linkplain URI#isAbsolute() absolute}.</p> 724 * 725 * @return the non-{@code null}, {@linkplain URI#isAbsolute() 726 * absolute} {@link URI} of the root of this {@link ChartRepository} 727 */ 728 public final URI getUri() { 729 return this.uri; 730 } 731 732 /** 733 * Returns a non-{@code null}, {@linkplain Path#isAbsolute() 734 * absolute} {@link Path} to the file that contains or will contain 735 * a copy of the chart repository's <a 736 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 737 * index.yaml}</a> file. 738 * 739 * <p>This method never returns {@code null}.</p> 740 * 741 * @return a non-{@code null}, {@linkplain Path#isAbsolute() 742 * absolute} {@link Path} to the file that contains or will contain 743 * a copy of the chart repository's <a 744 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 745 * index.yaml}</a> file 746 */ 747 public final Path getCachedIndexPath() { 748 return this.cachedIndexPath; 749 } 750 751 /** 752 * Returns the {@link Index} for this {@link ChartRepository}. 753 * 754 * <p>This method never returns {@code null}.</p> 755 * 756 * <p>If this method has not been invoked before on this {@link 757 * ChartRepository}, then the {@linkplain #getCachedIndexPath() 758 * cached copy} of the chart repository's <a 759 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 760 * index.yaml}</a> file is parsed into an {@link Index} and that 761 * {@link Index} is stored in an instance variable before it is 762 * returned.</p> 763 * 764 * <p>If no {@linkplain #getCachedIndexPath() cached copy} of the 765 * chart repository's <a 766 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 767 * index.yaml}</a> file exists, then one is {@linkplain 768 * #downloadIndex() downloaded} first.</p> 769 * 770 * return the {@link Index} representing the contents of this {@link 771 * ChartRepository}; never {@code null} 772 * 773 * @exception IOException if there was a problem either parsing an 774 * <a 775 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 776 * index.yaml}</a> file or downloading it 777 * 778 * @exception URISyntaxException if one of the URIs in the <a 779 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 780 * index.yaml}</a> file is invalid 781 * 782 * @see #getIndex(boolean, CopyOption...) 783 * 784 * @see #downloadIndex() 785 */ 786 public final Index getIndex() throws IOException, URISyntaxException { 787 return this.getIndex(false, StandardCopyOption.REPLACE_EXISTING); 788 } 789 790 /** 791 * Returns the {@link Index} for this {@link ChartRepository}. 792 * 793 * <p>This method never returns {@code null}.</p> 794 * 795 * <p>If this method has not been invoked before on this {@link 796 * ChartRepository}, then the {@linkplain #getCachedIndexPath() 797 * cached copy} of the chart repository's <a 798 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 799 * index.yaml}</a> file is parsed into an {@link Index} and that 800 * {@link Index} is stored in an instance variable before it is 801 * returned.</p> 802 * 803 * <p>If the {@linkplain #getCachedIndexPath() cached copy} of the 804 * chart repository's <a 805 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 806 * index.yaml}</a> file {@linkplain #isCachedIndexExpired() has 807 * expired}, then one is {@linkplain #downloadIndex() downloaded} 808 * first.</p> 809 * 810 * @param forceDownload if {@code true} then no caching will happen 811 * 812 * @return the {@link Index} representing the contents of this {@link 813 * ChartRepository}; never {@code null} 814 * 815 * @exception IOException if there was a problem either parsing an 816 * <a 817 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 818 * index.yaml}</a> file or downloading it 819 * 820 * @exception URISyntaxException if one of the URIs in the <a 821 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 822 * index.yaml}</a> file is invalid 823 * 824 * @see #getIndex(boolean, CopyOption...) 825 * 826 * @see #downloadIndexTo(Path, CopyOption...) 827 * 828 * @see #isCachedIndexExpired() 829 */ 830 public final Index getIndex(final boolean forceDownload) throws IOException, URISyntaxException { 831 return this.getIndex(forceDownload, StandardCopyOption.REPLACE_EXISTING); 832 } 833 834 /** 835 * Returns the {@link Index} for this {@link ChartRepository}. 836 * 837 * <p>This method never returns {@code null}.</p> 838 * 839 * <p>If this method has not been invoked before on this {@link 840 * ChartRepository}, then the {@linkplain #getCachedIndexPath() 841 * cached copy} of the chart repository's <a 842 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 843 * index.yaml}</a> file is parsed into an {@link Index} and that 844 * {@link Index} is stored in an instance variable before it is 845 * returned.</p> 846 * 847 * <p>If the {@linkplain #getCachedIndexPath() cached copy} of the 848 * chart repository's <a 849 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 850 * index.yaml}</a> file {@linkplain #isCachedIndexExpired() has 851 * expired}, then one is {@linkplain #downloadIndex() downloaded} 852 * first.</p> 853 * 854 * @param forceDownload if {@code true} then no caching will happen 855 * 856 * @param copyOptions any {@link CopyOption} instances that will be 857 * passed to any {@link Files#move(Path, Path, CopyOption...)} 858 * invocation that may be necessary; may be {@code null} 859 * 860 * @return the {@link Index} representing the contents of this {@link 861 * ChartRepository}; never {@code null} 862 * 863 * @exception IOException if there was a problem either parsing an 864 * <a 865 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 866 * index.yaml}</a> file or downloading it 867 * 868 * @exception URISyntaxException if one of the URIs in the <a 869 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 870 * index.yaml}</a> file is invalid 871 * 872 * @see #downloadIndexTo(Path, CopyOption...) 873 * 874 * @see #isCachedIndexExpired() 875 */ 876 @Issue(id = "156", uri = "https://github.com/microbean/microbean-helm/issues/156") 877 public final Index getIndex(final boolean forceDownload, final CopyOption... copyOptions) throws IOException, URISyntaxException { 878 if (forceDownload || this.index == null) { 879 final Path cachedIndexPath = this.getCachedIndexPath(); 880 assert cachedIndexPath != null; 881 if (forceDownload || this.isCachedIndexExpired()) { 882 this.downloadIndexTo(cachedIndexPath, copyOptions); 883 } 884 this.index = Index.loadFrom(cachedIndexPath); 885 assert this.index != null; 886 } 887 return this.index; 888 } 889 890 /** 891 * Returns {@code true} if the {@linkplain #getCachedIndexPath() 892 * cached copy} of the <a 893 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 894 * index.yaml}</a> file is to be considered stale. 895 * 896 * <p>The default implementation of this method returns the negation 897 * of the return value of an invocation of the {@link 898 * Files#isRegularFile(Path, LinkOption...)} method on the return value of the 899 * {@link #getCachedIndexPath()} method.</p> 900 * 901 * @return {@code true} if the {@linkplain #getCachedIndexPath() 902 * cached copy} of the <a 903 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 904 * index.yaml}</a> file is to be considered stale; {@code false} otherwise 905 * 906 * @see #getIndex(boolean) 907 */ 908 public boolean isCachedIndexExpired() { 909 final Path cachedIndexPath = this.getCachedIndexPath(); 910 assert cachedIndexPath != null; 911 return !Files.isRegularFile(cachedIndexPath); 912 } 913 914 /** 915 * Clears the {@link Index} stored internally by this {@link 916 * ChartRepository}, paving the way for a fresh copy to be installed 917 * by the {@link #getIndex(boolean)} method, and returns the old 918 * value. 919 * 920 * <p>This method may return {@code null} if {@code 921 * #getIndex(boolean)} has not yet been called.</p> 922 * 923 * @return the {@link Index}, or {@code null} 924 */ 925 public final Index clearIndex() { 926 final Index returnValue = this.index; 927 this.index = null; 928 return returnValue; 929 } 930 931 /** 932 * Invokes the {@link #downloadIndexTo(Path, CopyOption...)} method 933 * with the return value of the {@link #getCachedIndexPath()} method 934 * as its first parameter value, and {@link 935 * StandardCopyOption#REPLACE_EXISTING} as its second parameter 936 * value. 937 * 938 * <p>This method never returns {@code null}.</p> 939 * 940 * @return {@link Path} the {@link Path} to which the {@code 941 * index.yaml} file was downloaded; never {@code null} 942 * 943 * @exception IOException if there was a problem downloading 944 * 945 * @see #downloadIndexTo(Path, CopyOption...) 946 */ 947 public final Path downloadIndex() throws IOException { 948 return this.downloadIndexTo(this.getCachedIndexPath(), StandardCopyOption.REPLACE_EXISTING); 949 } 950 951 /** 952 * Invokes the {@link #downloadIndexTo(Path, CopyOption...)} method 953 * with the return value of the {@link #getCachedIndexPath()} 954 * method. 955 * 956 * <p>This method never returns {@code null}.</p> 957 * 958 * @param copyOptions any {@link CopyOption} instances that will be 959 * passed to any {@link Files#move(Path, Path, CopyOption...)} 960 * invocations that may be necessary; may be {@code null} 961 * 962 * @return {@link Path} the {@link Path} to which the {@code 963 * index.yaml} file was downloaded; never {@code null} 964 * 965 * @exception IOException if there was a problem downloading 966 * 967 * @see #downloadIndexTo(Path, CopyOption...) 968 */ 969 @Issue(id = "156", uri = "https://github.com/microbean/microbean-helm/issues/156") 970 public final Path downloadIndex(final CopyOption... copyOptions) throws IOException { 971 return this.downloadIndexTo(this.getCachedIndexPath(), copyOptions); 972 } 973 974 /** 975 * Downloads a copy of the chart repository's <a 976 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 977 * index.yaml}</a> file to the {@link Path} specified and returns 978 * the canonical representation of the {@link Path} to which the 979 * file was actually downloaded. 980 * 981 * <p>This method never returns {@code null}.</p> 982 * 983 * <p>Overrides of this method must not return {@code null}.</p> 984 * 985 * <p>The default implementation of this method actually downloads 986 * the <a 987 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 988 * index.yaml}</a> file to a {@linkplain 989 * Files#createTempFile(String, String, FileAttribute...) temporary 990 * file} first, and then renames it, replacing any existing file 991 * with that name.</p> 992 * 993 * @param path the {@link Path} to download the <a 994 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 995 * index.yaml}</a> file to; may be {@code null} in which case the 996 * return value of the {@link #getCachedIndexPath()} method will be 997 * used instead 998 * 999 * @return the {@link Path} to the file; never {@code null} 1000 * 1001 * @exception IOException if there was a problem downloading 1002 * 1003 * @see #downloadIndexTo(Path, CopyOption...) 1004 */ 1005 public Path downloadIndexTo(Path path) throws IOException { 1006 return this.downloadIndexTo(path, StandardCopyOption.REPLACE_EXISTING); 1007 } 1008 1009 /** 1010 * Downloads a copy of the chart repository's <a 1011 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 1012 * index.yaml}</a> file to the {@link Path} specified and returns 1013 * the canonical representation of the {@link Path} to which the 1014 * file was actually downloaded. 1015 * 1016 * <p>This method never returns {@code null}.</p> 1017 * 1018 * <p>Overrides of this method must not return {@code null}.</p> 1019 * 1020 * <p>The default implementation of this method actually downloads 1021 * the <a 1022 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 1023 * index.yaml}</a> file to a {@linkplain 1024 * Files#createTempFile(String, String, FileAttribute...) temporary 1025 * file} first, and then renames it, replacing any existing file 1026 * with that name.</p> 1027 * 1028 * @param path the {@link Path} to download the <a 1029 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 1030 * index.yaml}</a> file to; may be {@code null} in which case the 1031 * return value of the {@link #getCachedIndexPath()} method will be 1032 * used instead 1033 * 1034 * @param copyOptions any {@link CopyOption} instances that will be 1035 * passed to an {@link Files#move(Path, Path, CopyOption...)} 1036 * invocation; may be {@code null} 1037 * 1038 * @return the {@link Path} to the file; never {@code null} 1039 * 1040 * @exception IOException if there was a problem downloading 1041 */ 1042 @Issue(id = "156", uri = "https://github.com/microbean/microbean-helm/issues/156") 1043 public Path downloadIndexTo(Path path, final CopyOption... copyOptions) throws IOException { 1044 final URI baseUri = this.getUri(); 1045 if (baseUri == null) { 1046 throw new IllegalStateException("getUri() == null"); 1047 } 1048 final URI indexUri = baseUri.resolve("index.yaml"); 1049 assert indexUri != null; 1050 final URL indexUrl = indexUri.toURL(); 1051 assert indexUrl != null; 1052 if (path == null) { 1053 path = this.getCachedIndexPath(); 1054 } 1055 assert path != null; 1056 if (!path.isAbsolute()) { 1057 assert this.indexCacheDirectory != null; 1058 assert this.indexCacheDirectory.isAbsolute(); 1059 path = this.indexCacheDirectory.resolve(path); 1060 assert path != null; 1061 assert path.isAbsolute(); 1062 } 1063 final Path temporaryPath = Files.createTempFile(new StringBuilder(this.getName()).append("-index-").toString(), ".yaml"); 1064 assert temporaryPath != null; 1065 try (final BufferedInputStream stream = new BufferedInputStream(this.openStream(indexUrl))) { 1066 Files.copy(stream, temporaryPath, StandardCopyOption.REPLACE_EXISTING); 1067 } catch (final IOException throwMe) { 1068 try { 1069 Files.deleteIfExists(temporaryPath); 1070 } catch (final IOException suppressMe) { 1071 throwMe.addSuppressed(suppressMe); 1072 } 1073 throw throwMe; 1074 } 1075 final Path returnValue; 1076 if (copyOptions == null || copyOptions.length <= 0) { 1077 returnValue = Files.move(temporaryPath, path); 1078 } else { 1079 returnValue = Files.move(temporaryPath, path, copyOptions); 1080 } 1081 return returnValue; 1082 } 1083 1084 /** 1085 * Creates a new {@link Index} from the contents of the {@linkplain 1086 * #getCachedIndexPath() cached copy of the chart repository's 1087 * <code>index.yaml</code> file} and returns it. 1088 * 1089 * <p>This method never returns {@code null}.</p> 1090 * 1091 * <p>Overrides of this method must not return {@code null}.</p> 1092 * 1093 * @return a new {@link Index}; never {@code null} 1094 * 1095 * @exception IOException if there was a problem reading the file 1096 * 1097 * @exception URISyntaxException if a URI in the file was invalid 1098 * 1099 * @see Index#loadFrom(Path) 1100 */ 1101 public Index loadIndex() throws IOException, URISyntaxException { 1102 Path path = this.getCachedIndexPath(); 1103 assert path != null; 1104 if (!path.isAbsolute()) { 1105 assert this.indexCacheDirectory != null; 1106 assert this.indexCacheDirectory.isAbsolute(); 1107 path = this.indexCacheDirectory.resolve(path); 1108 assert path != null; 1109 assert path.isAbsolute(); 1110 } 1111 return Index.loadFrom(path); 1112 } 1113 1114 /** 1115 * Given a Helm chart name and its version, returns the local {@link 1116 * Path}, representing a local copy of the Helm chart as downloaded 1117 * from the chart repository represented by this {@link 1118 * ChartRepository}, downloading the archive if necessary, and 1119 * replacing any prior copy that may exist. 1120 * 1121 * <p>This method may return {@code null}.</p> 1122 * 1123 * @param chartName the name of the chart whose local {@link Path} 1124 * should be returned; must not be {@code null} 1125 * 1126 * @param chartVersion the version of the chart to select; may be 1127 * {@code null} in which case "latest" semantics are implied 1128 * 1129 * @return the {@link Path} to the chart archive, or {@code null} 1130 * 1131 * @exception IOException if there was a problem downloading 1132 * 1133 * @exception URISyntaxException if this {@link ChartRepository}'s 1134 * {@linkplain #getIndex() associated <code>Index</code>} could not 1135 * be parsed 1136 * 1137 * @exception NullPointerException if {@code chartName} is {@code 1138 * null} 1139 * 1140 * @see #getCachedChartPath(String, String, CopyOption...) 1141 */ 1142 public final Path getCachedChartPath(final String chartName, String chartVersion) throws IOException, URISyntaxException { 1143 return this.getCachedChartPath(chartName, chartVersion, StandardCopyOption.REPLACE_EXISTING); 1144 } 1145 1146 /** 1147 * Given a Helm chart name and its version, returns the local {@link 1148 * Path}, representing a local copy of the Helm chart as downloaded 1149 * from the chart repository represented by this {@link 1150 * ChartRepository}, downloading the archive if necessary. 1151 * 1152 * <p>This method may return {@code null}.</p> 1153 * 1154 * @param chartName the name of the chart whose local {@link Path} 1155 * should be returned; must not be {@code null} 1156 * 1157 * @param chartVersion the version of the chart to select; may be 1158 * {@code null} in which case "latest" semantics are implied 1159 * 1160 * @param copyOptions any {@link CopyOption} instances that will be 1161 * passed to any {@link Files#move(Path, Path, CopyOption...)} 1162 * invocations that may be necessary; may be {@code null} 1163 * 1164 * @return the {@link Path} to the chart archive, or {@code null} 1165 * 1166 * @exception IOException if there was a problem downloading 1167 * 1168 * @exception URISyntaxException if this {@link ChartRepository}'s 1169 * {@linkplain #getIndex() associated <code>Index</code>} could not 1170 * be parsed 1171 * 1172 * @exception NullPointerException if {@code chartName} is {@code 1173 * null} 1174 */ 1175 @Issue(id = "156", uri = "https://github.com/microbean/microbean-helm/issues/156") 1176 public final Path getCachedChartPath(final String chartName, String chartVersion, final CopyOption... copyOptions) throws IOException, URISyntaxException { 1177 Objects.requireNonNull(chartName); 1178 Path returnValue = null; 1179 if (chartVersion == null) { 1180 final Index index = this.getIndex(false); 1181 assert index != null; 1182 final Index.Entry entry = index.getEntry(chartName, null /* latest */); 1183 if (entry != null) { 1184 chartVersion = entry.getVersion(); 1185 } 1186 } 1187 if (chartVersion != null) { 1188 assert this.archiveCacheDirectory != null; 1189 final StringBuilder chartKey = new StringBuilder(chartName).append("-").append(chartVersion); 1190 final String chartFilename = new StringBuilder(chartKey).append(".tgz").toString(); 1191 final Path cachedChartPath = this.archiveCacheDirectory.resolve(chartFilename); 1192 assert cachedChartPath != null; 1193 if (!Files.isRegularFile(cachedChartPath)) { 1194 final Index index = this.getIndex(true); 1195 assert index != null; 1196 final Index.Entry entry = index.getEntry(chartName, chartVersion); 1197 if (entry != null) { 1198 URI chartUri = entry.getFirstUri(); 1199 if (chartUri != null) { 1200 1201 // See https://github.com/kubernetes/helm/issues/3057 1202 if (!chartUri.isAbsolute()) { 1203 final URI chartRepositoryUri = this.getUri(); 1204 assert chartRepositoryUri != null; 1205 assert chartRepositoryUri.isAbsolute(); 1206 chartUri = chartRepositoryUri.resolve(chartUri); 1207 assert chartUri != null; 1208 assert chartUri.isAbsolute(); 1209 } 1210 1211 final URL chartUrl = chartUri.toURL(); 1212 assert chartUrl != null; 1213 final Path temporaryPath = Files.createTempFile(chartKey.append("-").toString(), ".tgz"); 1214 assert temporaryPath != null; 1215 try (final InputStream stream = new BufferedInputStream(this.openStream(chartUrl))) { 1216 Files.copy(stream, temporaryPath, StandardCopyOption.REPLACE_EXISTING); 1217 } catch (final IOException throwMe) { 1218 try { 1219 Files.deleteIfExists(temporaryPath); 1220 } catch (final IOException suppressMe) { 1221 throwMe.addSuppressed(suppressMe); 1222 } 1223 throw throwMe; 1224 } 1225 if (copyOptions == null || copyOptions.length <= 0) { 1226 Files.move(temporaryPath, cachedChartPath); 1227 } else { 1228 Files.move(temporaryPath, cachedChartPath, copyOptions); 1229 } 1230 } 1231 } 1232 } 1233 returnValue = cachedChartPath; 1234 } 1235 return returnValue; 1236 } 1237 1238 /** 1239 * Returns an {@link InputStream} corresponding to the supplied 1240 * {@link URL}. 1241 * 1242 * <p>This method may return {@code null}.</p> 1243 * 1244 * <p>Overrides of this method are permitted to return {@code 1245 * null}.</p> 1246 * 1247 * @param url the {@link URL} whose affiliated {@link InputStream} 1248 * should be returned; may be {@code null} in which case {@code 1249 * null} will be returned 1250 * 1251 * @return an {@link InputStream} appropriate for the supplied 1252 * {@link URL}, or {@code null} 1253 * 1254 * @exception IOException if an error occurs while connecting to the 1255 * supplied {@link URL} 1256 */ 1257 protected InputStream openStream(final URL url) throws IOException { 1258 InputStream returnValue = null; 1259 if (url != null) { 1260 assert this.proxy != null; 1261 final URLConnection urlConnection = url.openConnection(this.proxy); 1262 assert urlConnection != null; 1263 urlConnection.setRequestProperty("User-Agent", "microbean-helm"); 1264 returnValue = urlConnection.getInputStream(); 1265 } 1266 return returnValue; 1267 } 1268 1269 /** 1270 * {@inheritDoc} 1271 * 1272 * <p>This implementation calls the {@link 1273 * #getCachedChartPath(String, String)} method with the supplied 1274 * arguments and uses a {@link TapeArchiveChartLoader} to load the 1275 * resulting archive into a {@link Chart.Builder} object.</p> 1276 */ 1277 @Override 1278 public Chart.Builder resolve(final String chartName, String chartVersion) throws ChartResolverException { 1279 Objects.requireNonNull(chartName); 1280 Chart.Builder returnValue = null; 1281 Path cachedChartPath = null; 1282 try { 1283 cachedChartPath = this.getCachedChartPath(chartName, chartVersion); 1284 } catch (final IOException | URISyntaxException exception) { 1285 throw new ChartResolverException(exception.getMessage(), exception); 1286 } 1287 if (cachedChartPath != null && Files.isRegularFile(cachedChartPath)) { 1288 try (final TapeArchiveChartLoader loader = new TapeArchiveChartLoader()) { 1289 returnValue = loader.load(new TarInputStream(new GZIPInputStream(new BufferedInputStream(Files.newInputStream(cachedChartPath))))); 1290 } catch (final IOException exception) { 1291 throw new ChartResolverException(exception.getMessage(), exception); 1292 } 1293 } 1294 return returnValue; 1295 } 1296 1297 /** 1298 * Returns a {@link Path} representing "Helm home": the root 1299 * directory for various Helm-related metadata as specified by 1300 * either the {@code helm.home} system property or the {@code 1301 * HELM_HOME} environment variable. 1302 * 1303 * <p>This method never returns {@code null}.</p> 1304 * 1305 * <p>No guarantee is made by this method regarding whether the 1306 * returned {@link Path} actually denotes a directory.</p> 1307 * 1308 * @return a {@link Path} representing "Helm home"; never {@code 1309 * null} 1310 * 1311 * @exception SecurityException if there are not sufficient 1312 * permissions to read system properties or environment variables 1313 */ 1314 @Deprecated 1315 static final Path getHelmHome() { 1316 return helmHome.toPath(); 1317 } 1318 1319 1320 /* 1321 * Inner and nested classes. 1322 */ 1323 1324 1325 /** 1326 * A class representing certain of the contents of a <a 1327 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">Helm 1328 * chart repository's {@code index.yaml} file</a>. 1329 * 1330 * @author <a href="https://about.me/lairdnelson" 1331 * target="_parent">Laird Nelson</a> 1332 */ 1333 @Experimental 1334 public static final class Index { 1335 1336 1337 /* 1338 * Instance fields. 1339 */ 1340 1341 1342 /** 1343 * An {@linkplain Collections#unmodifiableSortedMap(SortedMap) 1344 * immutable} {@link SortedMap} of {@link SortedSet}s of {@link 1345 * Entry} objects whose values represent enough information to 1346 * derive a URI to a Helm chart. 1347 * 1348 * <p>This field is never {@code null}.</p> 1349 */ 1350 private final SortedMap<String, SortedSet<Entry>> entries; 1351 1352 1353 /* 1354 * Constructors. 1355 */ 1356 1357 1358 /** 1359 * Creates a new {@link Index}. 1360 * 1361 * @param entries a {@link Map} of {@link SortedSet}s of {@link 1362 * Entry} objects indexed by the name of the Helm chart they 1363 * describe; may be {@code null}; copied by value 1364 */ 1365 Index(final Map<? extends String, ? extends SortedSet<Entry>> entries) { 1366 super(); 1367 if (entries == null || entries.isEmpty()) { 1368 this.entries = Collections.emptySortedMap(); 1369 } else { 1370 this.entries = Collections.unmodifiableSortedMap(deepCopy(entries)); 1371 } 1372 } 1373 1374 1375 /* 1376 * Instance methods. 1377 */ 1378 1379 1380 /** 1381 * Creates and returns a new {@link Index} consisting of all this 1382 * {@link Index} instance's {@linkplain #getEntries() entries} 1383 * augmented with those entries from the supplied {@link Index} 1384 * that this {@link Index} instance did not already contain. 1385 * 1386 * @param other the {@link Index} to merge in; may be {@code null} 1387 * 1388 * @return a new {@link Index} reflecting the merge operation 1389 */ 1390 @Experimental 1391 public final Index merge(final Index other) { 1392 final Index returnValue; 1393 final Map<String, SortedSet<Entry>> myEntries = this.getEntries(); 1394 final Map<String, SortedSet<Entry>> otherEntries; 1395 if (other == null) { 1396 otherEntries = null; 1397 } else { 1398 otherEntries = other.getEntries(); 1399 } 1400 if (otherEntries == null || otherEntries.isEmpty()) { 1401 if (myEntries == null || myEntries.isEmpty()) { 1402 returnValue = new Index(null); 1403 } else { 1404 returnValue = new Index(myEntries); 1405 } 1406 } else if (myEntries == null || myEntries.isEmpty()) { 1407 returnValue = new Index(otherEntries); 1408 } else { 1409 final Map<String, SortedSet<Entry>> mergedEntries = deepCopy(myEntries); 1410 final Set<Map.Entry<String, SortedSet<Entry>>> otherEntrySet = otherEntries.entrySet(); 1411 if (otherEntrySet != null && !otherEntrySet.isEmpty()) { 1412 for (final Map.Entry<? extends String, ? extends SortedSet<Entry>> otherEntrySetElement : otherEntrySet) { 1413 if (otherEntrySetElement != null) { 1414 final SortedSet<Entry> otherValues = otherEntrySetElement.getValue(); 1415 if (otherValues != null && !otherValues.isEmpty()) { 1416 for (final Entry otherEntry : otherValues) { 1417 if (otherEntry != null) { 1418 final String otherEntryName = otherEntry.getName(); 1419 final Entry myCorrespondingEntry = this.getEntry(otherEntryName, otherEntry.getVersion()); 1420 if (myCorrespondingEntry == null) { 1421 SortedSet<Entry> myRelatedEntries = mergedEntries.get(otherEntryName); 1422 if (myRelatedEntries == null) { 1423 myRelatedEntries = new TreeSet<>(Collections.reverseOrder()); 1424 mergedEntries.put(otherEntryName, myRelatedEntries); 1425 } 1426 assert !myRelatedEntries.contains(otherEntry); 1427 myRelatedEntries.add(otherEntry); 1428 } 1429 } 1430 } 1431 } 1432 } 1433 } 1434 } 1435 returnValue = new Index(mergedEntries); 1436 } 1437 return returnValue; 1438 } 1439 1440 /** 1441 * Returns a non-{@code null}, {@linkplain 1442 * Collections#unmodifiableMap(Map) immutable} {@link Map} of 1443 * {@link SortedSet}s of {@link Entry} objects, indexed by the 1444 * name of the Helm chart they describe. 1445 * 1446 * @return a non-{@code null}, {@linkplain 1447 * Collections#unmodifiableMap(Map) immutable} {@link Map} of 1448 * {@link SortedSet}s of {@link Entry} objects, indexed by the 1449 * name of the Helm chart they describe 1450 */ 1451 public final Map<String, SortedSet<Entry>> getEntries() { 1452 return this.entries; 1453 } 1454 1455 /** 1456 * Returns an {@link Entry} identified by the supplied {@code 1457 * name} and {@code version}, if there is one. 1458 * 1459 * <p>This method may return {@code null}.</p> 1460 * 1461 * @param name the name of the Helm chart whose related {@link 1462 * Entry} is desired; must not be {@code null} 1463 * 1464 * @param versionString the version of the Helm chart whose 1465 * related {@link Entry} is desired; may be {@code null} in which 1466 * case "latest" semantics are implied 1467 * 1468 * @return an {@link Entry}, or {@code null} 1469 * 1470 * @exception NullPointerException if {@code name} is {@code null} 1471 */ 1472 public final Entry getEntry(final String name, final String versionString) { 1473 Objects.requireNonNull(name); 1474 Entry returnValue = null; 1475 final Map<String, SortedSet<Entry>> entries = this.getEntries(); 1476 if (entries != null && !entries.isEmpty()) { 1477 final SortedSet<Entry> entrySet = entries.get(name); 1478 if (entrySet != null && !entrySet.isEmpty()) { 1479 if (versionString == null) { 1480 returnValue = entrySet.first(); 1481 } else { 1482 for (final Entry entry : entrySet) { 1483 // XXX TODO FIXME: probably want to make this a 1484 // constraint match, not just an equality comparison 1485 if (entry != null && versionString.equals(entry.getVersion())) { 1486 returnValue = entry; 1487 break; 1488 } 1489 } 1490 } 1491 } 1492 } 1493 return returnValue; 1494 } 1495 1496 1497 /* 1498 * Static methods. 1499 */ 1500 1501 1502 /** 1503 * Creates a new {@link Index} whose contents are sourced from the 1504 * YAML file located at the supplied {@link Path}. 1505 * 1506 * <p>This method never returns {@code null}.</p> 1507 * 1508 * @param path the {@link Path} to a YAML file whose contents are 1509 * those of a <a 1510 * href="https://docs.helm.sh/developing_charts/#the-index-file">Helm 1511 * chart repository index</a>; must not be {@code null} 1512 * 1513 * @return a new {@link Index}; never {@code null} 1514 * 1515 * @exception IOException if there was a problem reading the file 1516 * 1517 * @exception URISyntaxException if one of the URIs in the file 1518 * was invalid 1519 * 1520 * @exception NullPointerException if {@code path} is {@code null} 1521 * 1522 * @see #loadFrom(InputStream) 1523 */ 1524 public static final Index loadFrom(final Path path) throws IOException, URISyntaxException { 1525 Objects.requireNonNull(path); 1526 final Index returnValue; 1527 try (final BufferedInputStream stream = new BufferedInputStream(Files.newInputStream(path))) { 1528 returnValue = loadFrom(stream); 1529 } 1530 return returnValue; 1531 } 1532 1533 /** 1534 * Creates a new {@link Index} whose contents are sourced from the 1535 * <a 1536 * href="https://docs.helm.sh/developing_charts/#the-index-file">Helm 1537 * chart repository index</a> YAML contents represented by the 1538 * supplied {@link InputStream}. 1539 * 1540 * <p>This method never returns {@code null}.</p> 1541 * 1542 * @param stream the {@link InputStream} to a YAML file whose contents are 1543 * those of a <a 1544 * href="https://docs.helm.sh/developing_charts/#the-index-file">Helm 1545 * chart repository index</a>; must not be {@code null} 1546 * 1547 * @return a new {@link Index}; never {@code null} 1548 * 1549 * @exception IOException if there was a problem reading the file 1550 * 1551 * @exception URISyntaxException if one of the URIs in the file 1552 * was invalid 1553 * 1554 * @exception NullPointerException if {@code path} is {@code null} 1555 */ 1556 public static final Index loadFrom(final InputStream stream) throws IOException, URISyntaxException { 1557 Objects.requireNonNull(stream); 1558 final Index returnValue; 1559 @Issue(id = "131", uri = "https://github.com/microbean/microbean-helm/issues/131") 1560 final Map<?, ?> yamlMap = new Yaml(new SafeConstructor(), new Representer(), new DumperOptions(), new StringResolver()).load(stream); 1561 if (yamlMap == null || yamlMap.isEmpty()) { 1562 returnValue = new Index(null); 1563 } else { 1564 final SortedMap<String, SortedSet<Index.Entry>> sortedEntryMap = new TreeMap<>(); 1565 @SuppressWarnings("unchecked") 1566 final Map<? extends String, ? extends Collection<? extends Map<?, ?>>> entriesMap = (Map<? extends String, ? extends Collection<? extends Map<?, ?>>>)yamlMap.get("entries"); 1567 if (entriesMap != null && !entriesMap.isEmpty()) { 1568 final Collection<? extends Map.Entry<? extends String, ? extends Collection<? extends Map<?, ?>>>> entries = entriesMap.entrySet(); 1569 if (entries != null && !entries.isEmpty()) { 1570 for (final Map.Entry<? extends String, ? extends Collection<? extends Map<?, ?>>> mapEntry : entries) { 1571 if (mapEntry != null) { 1572 final String entryName = mapEntry.getKey(); 1573 if (entryName != null) { 1574 final Collection<? extends Map<?, ?>> entryContents = mapEntry.getValue(); 1575 if (entryContents != null && !entryContents.isEmpty()) { 1576 for (final Map<?, ?> entryMap : entryContents) { 1577 if (entryMap != null && !entryMap.isEmpty()) { 1578 final Metadata.Builder metadataBuilder = Metadata.newBuilder(); 1579 assert metadataBuilder != null; 1580 Metadatas.populateMetadataBuilder(metadataBuilder, entryMap); 1581 @SuppressWarnings("unchecked") 1582 final Collection<? extends String> uriStrings = (Collection<? extends String>)entryMap.get("urls"); 1583 Set<URI> uris = new LinkedHashSet<>(); 1584 if (uriStrings != null && !uriStrings.isEmpty()) { 1585 for (final String uriString : uriStrings) { 1586 if (uriString != null && !uriString.isEmpty()) { 1587 uris.add(new URI(uriString)); 1588 } 1589 } 1590 } 1591 final String digest = (String)entryMap.get("digest"); 1592 SortedSet<Index.Entry> entryObjects = sortedEntryMap.get(entryName); 1593 if (entryObjects == null) { 1594 entryObjects = new TreeSet<>(Collections.reverseOrder()); 1595 sortedEntryMap.put(entryName, entryObjects); 1596 } 1597 entryObjects.add(new Index.Entry(metadataBuilder, uris, digest)); 1598 } 1599 } 1600 } 1601 } 1602 } 1603 } 1604 } 1605 } 1606 returnValue = new Index(sortedEntryMap); 1607 } 1608 return returnValue; 1609 } 1610 1611 /** 1612 * Performs a deep copy of the supplied {@link Map} such that the 1613 * {@link SortedMap} returned has copies of the supplied {@link 1614 * Map}'s {@linkplain Map#values() values}. 1615 * 1616 * <p>This method may return {@code null} if {@code source} is 1617 * {@code null}.</p> 1618 * 1619 * <p>The {@link SortedMap} returned by this method is 1620 * mutable.</p> 1621 * 1622 * @param source the {@link Map} to copy; may be {@code null} in 1623 * which case {@code null} will be returned 1624 * 1625 * @return a mutable {@link SortedMap}, or {@code null} 1626 */ 1627 private static final SortedMap<String, SortedSet<Entry>> deepCopy(final Map<? extends String, ? extends SortedSet<Entry>> source) { 1628 final SortedMap<String, SortedSet<Entry>> returnValue; 1629 if (source == null) { 1630 returnValue = null; 1631 } else if (source.isEmpty()) { 1632 returnValue = Collections.emptySortedMap(); 1633 } else { 1634 returnValue = new TreeMap<>(); 1635 final Collection<? extends Map.Entry<? extends String, ? extends SortedSet<Entry>>> entrySet = source.entrySet(); 1636 if (entrySet != null && !entrySet.isEmpty()) { 1637 for (final Map.Entry<? extends String, ? extends SortedSet<Entry>> entry : entrySet) { 1638 final String key = entry.getKey(); 1639 final SortedSet<Entry> value = entry.getValue(); 1640 if (value == null) { 1641 returnValue.put(key, null); 1642 } else { 1643 final SortedSet<Entry> newValue = new TreeSet<>(value.comparator()); 1644 newValue.addAll(value); 1645 returnValue.put(key, newValue); 1646 } 1647 } 1648 } 1649 } 1650 return returnValue; 1651 } 1652 1653 1654 /* 1655 * Inner and nested classes. 1656 */ 1657 1658 1659 /** 1660 * An entry in a <a 1661 * href="https://docs.helm.sh/developing_charts/#the-index-file">Helm 1662 * chart repository index</a>. 1663 * 1664 * @author <a href="https://about.me/lairdnelson" 1665 * target="_parent">Laird Nelson</a> 1666 */ 1667 @Experimental 1668 public static final class Entry implements Comparable<Entry> { 1669 1670 1671 /* 1672 * Instance fields. 1673 */ 1674 1675 1676 /** 1677 * A {@link MetadataOrBuilder} representing most of the contents 1678 * of the entry. 1679 * 1680 * <p>This field is never {@code null}.</p> 1681 */ 1682 private final MetadataOrBuilder metadata; 1683 1684 /** 1685 * An {@linkplain Collections#unmodifiableSet(Set) immutable} 1686 * {@link Set} of {@link URI}s describing where the particular 1687 * Helm chart described by this {@link Entry} may be downloaded 1688 * from. 1689 * 1690 * <p>This field is never {@code null}.</p> 1691 */ 1692 private final Set<URI> uris; 1693 1694 private final String digest; 1695 1696 1697 /* 1698 * Constructors. 1699 */ 1700 1701 1702 /** 1703 * Creates a new {@link Entry}. 1704 * 1705 * @param metadata a {@link MetadataOrBuilder} representing most 1706 * of the contents of the entry; must not be {@code null} 1707 * 1708 * @param uris a {@link Collection} of {@link URI}s describing 1709 * where the particular Helm chart described by this {@link 1710 * Entry} may be downloaded from; may be {@code null}; copied by 1711 * value 1712 * 1713 * @exception NullPointerException if {@code metadata} is {@code 1714 * null} 1715 * 1716 * @see #Entry(MetadataOrBuilder, Collection, String) 1717 */ 1718 Entry(final MetadataOrBuilder metadata, final Collection<? extends URI> uris) { 1719 this(metadata, uris, null); 1720 } 1721 1722 /** 1723 * Creates a new {@link Entry}. 1724 * 1725 * @param metadata a {@link MetadataOrBuilder} representing most 1726 * of the contents of the entry; must not be {@code null} 1727 * 1728 * @param uris a {@link Collection} of {@link URI}s describing 1729 * where the particular Helm chart described by this {@link 1730 * Entry} may be downloaded from; may be {@code null}; copied by 1731 * value 1732 * 1733 * @param digest a SHA-256 message digest to be associated with 1734 * this {@link Entry}; may be {@code null} 1735 * 1736 * @exception NullPointerException if {@code metadata} is {@code 1737 * null} 1738 */ 1739 Entry(final MetadataOrBuilder metadata, final Collection<? extends URI> uris, final String digest) { 1740 super(); 1741 this.metadata = Objects.requireNonNull(metadata); 1742 if (uris == null || uris.isEmpty()) { 1743 this.uris = Collections.emptySet(); 1744 } else { 1745 this.uris = new LinkedHashSet<>(uris); 1746 } 1747 this.digest = digest; 1748 } 1749 1750 1751 /* 1752 * Instance methods. 1753 */ 1754 1755 1756 /** 1757 * Compares this {@link Entry} to the supplied {@link Entry} and 1758 * returns a value less than {@code 0} if this {@link Entry} is 1759 * "less than" the supplied {@link Entry}, {@code 1} if this 1760 * {@link Entry} is "greater than" the supplied {@link Entry} 1761 * and {@code 0} if this {@link Entry} is equal to the supplied 1762 * {@link Entry}. 1763 * 1764 * <p>{@link Entry} objects are compared by {@linkplain 1765 * #getName() name} first, then {@linkplain #getVersion() 1766 * version}.</p> 1767 * 1768 * <p>It is intended that this {@link 1769 * #compareTo(ChartRepository.Index.Entry)} method is 1770 * {@linkplain Comparable consistent with equals}.</p> 1771 * 1772 * @param her the {@link Entry} to compare; must not be {@code null} 1773 * 1774 * @return a value less than {@code 0} if this {@link Entry} is 1775 * "less than" the supplied {@link Entry}, {@code 1} if this 1776 * {@link Entry} is "greater than" the supplied {@link Entry} 1777 * and {@code 0} if this {@link Entry} is equal to the supplied 1778 * {@link Entry} 1779 * 1780 * @exception NullPointerException if the supplied {@link Entry} 1781 * is {@code null} 1782 */ 1783 @Override 1784 public final int compareTo(final Entry her) { 1785 Objects.requireNonNull(her); // see Comparable documentation 1786 1787 final String myName = this.getName(); 1788 final String herName = her.getName(); 1789 if (myName == null) { 1790 if (herName != null) { 1791 return -1; 1792 } 1793 } else if (herName == null) { 1794 return 1; 1795 } else { 1796 final int nameComparison = myName.compareTo(herName); 1797 if (nameComparison != 0) { 1798 return nameComparison; 1799 } 1800 } 1801 1802 final String myVersionString = this.getVersion(); 1803 final String herVersionString = her.getVersion(); 1804 if (myVersionString == null) { 1805 if (herVersionString != null) { 1806 return -1; 1807 } 1808 } else if (herVersionString == null) { 1809 return 1; 1810 } else { 1811 Version myVersion = null; 1812 try { 1813 myVersion = Version.valueOf(myVersionString); 1814 } catch (final IllegalArgumentException | ParseException badVersion) { 1815 myVersion = null; 1816 } 1817 Version herVersion = null; 1818 try { 1819 herVersion = Version.valueOf(herVersionString); 1820 } catch (final IllegalArgumentException | ParseException badVersion) { 1821 herVersion = null; 1822 } 1823 if (myVersion == null) { 1824 if (herVersion != null) { 1825 return -1; 1826 } 1827 } else if (herVersion == null) { 1828 return 1; 1829 } else { 1830 return myVersion.compareTo(herVersion); 1831 } 1832 } 1833 1834 return 0; 1835 } 1836 1837 /** 1838 * Returns a hashcode for this {@link Entry} based off its 1839 * {@linkplain #getName() name} and {@linkplain #getVersion() 1840 * version}. 1841 * 1842 * @return a hashcode for this {@link Entry} 1843 * 1844 * @see #compareTo(ChartRepository.Index.Entry) 1845 * 1846 * @see #equals(Object) 1847 * 1848 * @see #getName() 1849 * 1850 * @see #getVersion() 1851 */ 1852 @Override 1853 public final int hashCode() { 1854 int hashCode = 17; 1855 1856 final Object name = this.getName(); 1857 int c = name == null ? 0 : name.hashCode(); 1858 hashCode = 37 * hashCode + c; 1859 1860 final Object version = this.getVersion(); 1861 c = version == null ? 0 : version.hashCode(); 1862 hashCode = 37 * hashCode + c; 1863 1864 return hashCode; 1865 } 1866 1867 /** 1868 * Returns {@code true} if the supplied {@link Object} is an 1869 * {@link Entry} and has a {@linkplain #getName() name} and 1870 * {@linkplain #getVersion() version} equal to those of this 1871 * {@link Entry}. 1872 * 1873 * @param other the {@link Object} to test; may be {@code null} 1874 * in which case {@code false} will be returned 1875 * 1876 * @return {@code true} if this {@link Entry} is equal to the 1877 * supplied {@link Object}; {@code false} otherwise 1878 * 1879 * @see #compareTo(ChartRepository.Index.Entry) 1880 * 1881 * @see #getName() 1882 * 1883 * @see #getVersion() 1884 * 1885 * @see #hashCode() 1886 */ 1887 @Override 1888 public final boolean equals(final Object other) { 1889 if (other == this) { 1890 return true; 1891 } else if (other instanceof Entry) { 1892 final Entry her = (Entry)other; 1893 1894 final Object myName = this.getName(); 1895 if (myName == null) { 1896 if (her.getName() != null) { 1897 return false; 1898 } 1899 } else if (!myName.equals(her.getName())) { 1900 return false; 1901 } 1902 1903 final Object myVersion = this.getVersion(); 1904 if (myVersion == null) { 1905 if (her.getVersion() != null) { 1906 return false; 1907 } 1908 } else if (!myVersion.equals(her.getVersion())) { 1909 return false; 1910 } 1911 1912 return true; 1913 } else { 1914 return false; 1915 } 1916 } 1917 1918 /** 1919 * Returns the {@link MetadataOrBuilder} that comprises most of 1920 * the contents of this {@link Entry}. 1921 * 1922 * <p>This method never returns {@code null}.</p> 1923 * 1924 * @return the {@link MetadataOrBuilder} that comprises most of 1925 * the contents of this {@link Entry}; never {@code null} 1926 */ 1927 public final MetadataOrBuilder getMetadataOrBuilder() { 1928 return this.metadata; 1929 } 1930 1931 /** 1932 * Returns the return value of invoking the {@link 1933 * MetadataOrBuilder#getName()} method on the {@link 1934 * MetadataOrBuilder} returned by this {@link Entry}'s {@link 1935 * #getMetadataOrBuilder()} method. 1936 * 1937 * <p>This method may return {@code null}.</p> 1938 * 1939 * @return this {@link Entry}'s name, or {@code null} 1940 * 1941 * @see MetadataOrBuilder#getName() 1942 */ 1943 public final String getName() { 1944 final MetadataOrBuilder metadata = this.getMetadataOrBuilder(); 1945 assert metadata != null; 1946 return metadata.getName(); 1947 } 1948 1949 /** 1950 * Returns the return value of invoking the {@link 1951 * MetadataOrBuilder#getVersion()} method on the {@link 1952 * MetadataOrBuilder} returned by this {@link Entry}'s {@link 1953 * #getMetadataOrBuilder()} method. 1954 * 1955 * <p>This method may return {@code null}.</p> 1956 * 1957 * @return this {@link Entry}'s version, or {@code null} 1958 * 1959 * @see MetadataOrBuilder#getVersion() 1960 */ 1961 public final String getVersion() { 1962 final MetadataOrBuilder metadata = this.getMetadataOrBuilder(); 1963 assert metadata != null; 1964 return metadata.getVersion(); 1965 } 1966 1967 /** 1968 * Returns a non-{@code null}, {@linkplain 1969 * Collections#unmodifiableSet(Set) immutable} {@link Set} of 1970 * {@link URI}s representing the URIs from which the Helm chart 1971 * described by this {@link Entry} may be downloaded. 1972 * 1973 * <p>This method never returns {@code null}.</p> 1974 * 1975 * @return a non-{@code null}, {@linkplain 1976 * Collections#unmodifiableSet(Set) immutable} {@link Set} of 1977 * {@link URI}s representing the URIs from which the Helm chart 1978 * described by this {@link Entry} may be downloaded 1979 * 1980 * @see #getFirstUri() 1981 */ 1982 public final Set<URI> getUris() { 1983 return this.uris; 1984 } 1985 1986 /** 1987 * A convenience method that returns the first {@link URI} in 1988 * the {@link Set} of {@link URI}s returned by the {@link 1989 * #getUris()} method. 1990 * 1991 * <p>This method may return {@code null}.</p> 1992 * 1993 * @return the {@linkplain SortedSet#first() first} {@link URI} 1994 * in the {@link Set} of {@link URI}s returned by the {@link 1995 * #getUris()} method, or {@code null} 1996 * 1997 * @see #getUris() 1998 */ 1999 public final URI getFirstUri() { 2000 final Set<URI> uris = this.getUris(); 2001 final URI returnValue; 2002 if (uris == null || uris.isEmpty()) { 2003 returnValue = null; 2004 } else { 2005 final Iterator<URI> iterator = uris.iterator(); 2006 if (iterator == null || !iterator.hasNext()) { 2007 returnValue = null; 2008 } else { 2009 returnValue = iterator.next(); 2010 } 2011 } 2012 return returnValue; 2013 } 2014 2015 /** 2016 * Returns the SHA-256 message digest, in hexadecimal-encoded 2017 * {@link String} form, associated with this {@link Entry}. 2018 * 2019 * <p>This method may return {@code null}.</p> 2020 * 2021 * @return the SHA-256 message digest, in hexadecimal-encoded 2022 * {@link String} form, associated with this {@link Entry}, or 2023 * {@code null} 2024 */ 2025 public final String getDigest() { 2026 return this.digest; 2027 } 2028 2029 /** 2030 * Returns a non-{@code null} {@link String} representation of 2031 * this {@link Entry}. 2032 * 2033 * @return a non-{@code null} {@link String} representation of 2034 * this {@link Entry} 2035 */ 2036 @Override 2037 public final String toString() { 2038 String name = this.getName(); 2039 if (name == null || name.isEmpty()) { 2040 name = "unnamed"; 2041 } 2042 return new StringBuilder(name).append(" ").append(this.getVersion()).toString(); 2043 } 2044 2045 /** 2046 * Computes a SHA-256 message digest of the bytes readable from 2047 * the supplied {@link InputStream} and returns the result of 2048 * {@linkplain DatatypeConverter#printHexBinary(byte[]) 2049 * hexadecimal-encoding it}. 2050 * 2051 * <p>This method never returns {@code null}.</p> 2052 * 2053 * @param inputStream the {@link InputStream} to read from; must 2054 * not be {@code null} 2055 * 2056 * @return a {@linkplain 2057 * DatatypeConverter#printHexBinary(byte[]) hexadecimal-encoded} 2058 * SHA-256 message digest; never {@code null} 2059 * 2060 * @exception NullPointerException if {@code inputStream} is 2061 * {@code null} 2062 * 2063 * @exception IOException if an input or output error occurs 2064 */ 2065 @Experimental 2066 public static final String getDigest(final InputStream inputStream) throws IOException { 2067 Objects.requireNonNull(inputStream); 2068 MessageDigest md = null; 2069 try { 2070 md = MessageDigest.getInstance("SHA-256"); 2071 } catch (final NoSuchAlgorithmException noSuchAlgorithmException) { 2072 // SHA-256 is guaranteed to exist. 2073 throw new InternalError(noSuchAlgorithmException); 2074 } 2075 assert md != null; 2076 final ByteBuffer buffer = toByteBuffer(inputStream); 2077 assert buffer != null; 2078 md.update(buffer); 2079 return DatatypeConverter.printHexBinary(md.digest()); 2080 } 2081 2082 /** 2083 * Returns a {@link ByteBuffer} representing the supplied {@link 2084 * InputStream}. 2085 * 2086 * <p>This method never returns {@code null}.</p> 2087 * 2088 * @param stream the {@link InputStream} to represent; may be 2089 * {@code null} 2090 * 2091 * @return a non-{@code null} {@link ByteBuffer} 2092 * 2093 * @exception IOException if an input or output error occurs 2094 */ 2095 private static final ByteBuffer toByteBuffer(final InputStream stream) throws IOException { 2096 return ByteBuffer.wrap(read(stream)); 2097 } 2098 2099 /** 2100 * Fully reads the supplied {@link InputStream} into a {@code 2101 * byte} array and returns it. 2102 * 2103 * <p>This method never returns {@code null}.</p> 2104 * 2105 * @param stream the {@link InputStream} to read; may be {@code 2106 * null} 2107 * 2108 * @return a non-{@code null} {@code byte} array containing the 2109 * readable contents of the supplied {@link InputStream} 2110 * 2111 * @exception IOException if an input or output error occurs 2112 */ 2113 private static final byte[] read(final InputStream stream) throws IOException { 2114 byte[] returnValue = null; 2115 if (stream == null) { 2116 returnValue = new byte[0]; 2117 } else { 2118 try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { 2119 int bytesRead; 2120 final byte[] byteArray = new byte[4096]; 2121 while ((bytesRead = stream.read(byteArray, 0, byteArray.length)) != -1) { 2122 buffer.write(byteArray, 0, bytesRead); 2123 } 2124 buffer.flush(); 2125 returnValue = buffer.toByteArray(); 2126 } 2127 } 2128 return returnValue; 2129 } 2130 2131 2132 } 2133 2134 } 2135 2136}