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.URI; 025import java.net.URISyntaxException; 026import java.net.URL; 027import java.net.URLConnection; 028 029import java.nio.ByteBuffer; 030 031import java.nio.file.LinkOption; // for javadoc only 032import java.nio.file.StandardCopyOption; 033import java.nio.file.Files; 034import java.nio.file.Path; 035import java.nio.file.Paths; 036 037import java.nio.file.attribute.FileAttribute; // for javadoc only 038 039import java.security.MessageDigest; 040import java.security.NoSuchAlgorithmException; 041 042import java.util.Collection; 043import java.util.Collections; 044import java.util.Iterator; 045import java.util.Map; 046import java.util.Objects; 047import java.util.LinkedHashSet; 048import java.util.Set; 049import java.util.SortedSet; 050import java.util.SortedMap; 051import java.util.TreeSet; 052import java.util.TreeMap; 053 054import java.util.zip.GZIPInputStream; 055 056import javax.xml.bind.DatatypeConverter; 057 058import com.github.zafarkhaja.semver.ParseException; 059import com.github.zafarkhaja.semver.Version; 060 061import hapi.chart.ChartOuterClass.Chart; 062import hapi.chart.MetadataOuterClass.Metadata; 063import hapi.chart.MetadataOuterClass.MetadataOrBuilder; 064 065import org.kamranzafar.jtar.TarInputStream; 066 067import org.microbean.development.annotation.Experimental; 068 069import org.microbean.helm.chart.Metadatas; 070import org.microbean.helm.chart.TapeArchiveChartLoader; 071 072import org.microbean.helm.chart.resolver.AbstractChartResolver; 073import org.microbean.helm.chart.resolver.ChartResolverException; 074 075import org.yaml.snakeyaml.Yaml; 076 077/** 078 * An {@link AbstractChartResolver} that {@linkplain #resolve(String, 079 * String) resolves} <a 080 * href="https://docs.helm.sh/developing_charts/#charts">Helm 081 * charts</a> from <a 082 * href="https://docs.helm.sh/developing_charts/#create-a-chart-repository">a 083 * given Helm chart repository</a>. 084 * 085 * @author <a href="https://about.me/lairdnelson" 086 * target="_parent">Laird Nelson</a> 087 * 088 * @see #resolve(String, String) 089 */ 090@Experimental 091public class ChartRepository extends AbstractChartResolver { 092 093 094 /* 095 * Instance fields. 096 */ 097 098 099 /** 100 * An {@linkplain Path#isAbsolute() absolute} {@link Path} 101 * representing a directory where Helm chart archives may be stored. 102 * 103 * <p>This field will never be {@code null}.</p> 104 */ 105 private final Path archiveCacheDirectory; 106 107 /** 108 * An {@linkplain Path#isAbsolute() absolute} or relative {@link 109 * Path} representing a local copy of a chart repository's <a 110 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 111 * index.yaml}</a> file. 112 * 113 * <p>If the value of this field is a relative {@link Path}, then it 114 * will be considered to be relative to the value of the {@link 115 * #indexCacheDirectory} field.</p> 116 * 117 * <p>This field will never be {@code null}.</p> 118 * 119 * @see #getCachedIndexPath() 120 */ 121 private final Path cachedIndexPath; 122 123 /** 124 * The {@link Index} object representing the chart repository index 125 * as described canonically by its <a 126 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 127 * index.yaml}</a> file. 128 * 129 * <p>This field may be {@code null}.</p> 130 * 131 * @see #getIndex() 132 * 133 * @see #downloadIndex() 134 */ 135 private transient Index index; 136 137 /** 138 * An {@linkplain Path#isAbsolute() absolute} {@link Path} 139 * representing a directory that the value of the {@link 140 * #cachedIndexPath} field will be considered to be relative to. 141 * 142 * <p>This field may be {@code null}, in which case it is guaranteed 143 * that the {@link #cachedIndexPath} field's value is {@linkplain 144 * Path#isAbsolute() absolute}.</p> 145 */ 146 private final Path indexCacheDirectory; 147 148 /** 149 * The name of this {@link ChartRepository}. 150 * 151 * <p>This field is never {@code null}.</p> 152 * 153 * @see #getName() 154 */ 155 private final String name; 156 157 /** 158 * The {@link URI} representing the root of the chart repository 159 * represented by this {@link ChartRepository}. 160 * 161 * <p>This field is never {@code null}.</p> 162 * 163 * @see #getUri() 164 */ 165 private final URI uri; 166 167 168 /* 169 * Constructors. 170 */ 171 172 173 /** 174 * Creates a new {@link ChartRepository} whose {@linkplain 175 * #getCachedIndexPath() cached index path} will be a {@link Path} 176 * relative to the absolute directory represented by the value of 177 * the {@code helm.home} system property, or the value of the {@code 178 * HELM_HOME} environment variable, and bearing a name consisting of 179 * the supplied {@code name} suffixed with {@code -index.yaml}. 180 * 181 * @param name the name of this {@link ChartRepository}; must not be 182 * {@code null} 183 * 184 * @param uri the {@linkplain URI#isAbsolute() absolute} {@link URI} 185 * to the root of this {@link ChartRepository}; must not be {@code 186 * null} 187 * 188 * @exception NullPointerException if either {@code name} or {@code 189 * uri} is {@code null} 190 * 191 * @exception IllegalArgumentException if {@code uri} is {@linkplain 192 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 193 * home" directory 194 * 195 * @see #ChartRepository(String, URI, Path, Path, Path) 196 * 197 * @see #getName() 198 * 199 * @see #getUri() 200 * 201 * @see #getCachedIndexPath() 202 */ 203 public ChartRepository(final String name, final URI uri) { 204 this(name, uri, null, null, null); 205 } 206 207 /** 208 * Creates a new {@link ChartRepository}. 209 * 210 * @param name the name of this {@link ChartRepository}; must not be 211 * {@code null} 212 * 213 * @param uri the {@link URI} to the root of this {@link 214 * ChartRepository}; must not be {@code null} 215 * 216 * @param cachedIndexPath a {@link Path} naming the file that will 217 * store a copy of the chart repository's {@code index.yaml} file; 218 * if {@code null} then a {@link Path} relative to the absolute 219 * directory represented by the value of the {@code helm.home} 220 * system property, or the value of the {@code HELM_HOME} 221 * environment variable, and bearing a name consisting of the 222 * supplied {@code name} suffixed with {@code -index.yaml} will be 223 * used instead 224 * 225 * @exception NullPointerException if either {@code name} or {@code 226 * uri} is {@code null} 227 * 228 * @exception IllegalArgumentException if {@code uri} is {@linkplain 229 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 230 * home" directory 231 * 232 * @see #ChartRepository(String, URI, Path, Path, Path) 233 * 234 * @see #getName() 235 * 236 * @see #getUri() 237 * 238 * @see #getCachedIndexPath() 239 */ 240 public ChartRepository(final String name, final URI uri, final Path cachedIndexPath) { 241 this(name, uri, null, null, cachedIndexPath); 242 } 243 244 /** 245 * Creates a new {@link ChartRepository}. 246 * 247 * @param name the name of this {@link ChartRepository}; must not be 248 * {@code null} 249 * 250 * @param uri the {@link URI} to the root of this {@link 251 * ChartRepository}; must not be {@code null} 252 * 253 * @param archiveCacheDirectory an {@linkplain Path#isAbsolute() 254 * absolute} {@link Path} representing a directory where Helm chart 255 * archives may be stored; if {@code null} then a {@link Path} 256 * beginning with the absolute directory represented by the value of 257 * the {@code helm.home} system property, or the value of the {@code 258 * HELM_HOME} environment variable, appended with {@code 259 * cache/archive} will be used instead 260 * 261 * @param indexCacheDirectory an {@linkplain Path#isAbsolute() 262 * absolute} {@link Path} representing a directory that the supplied 263 * {@code cachedIndexPath} parameter value will be considered to be 264 * relative to; <strong>will be ignored and hence may be {@code 265 * null}</strong> if the supplied {@code cachedIndexPath} parameter 266 * value {@linkplain Path#isAbsolute() is absolute} 267 * 268 * @param cachedIndexPath a {@link Path} naming the file that will 269 * store a copy of the chart repository's {@code index.yaml} file; 270 * if {@code null} then a {@link Path} relative to the absolute 271 * directory represented by the value of the {@code helm.home} 272 * system property, or the value of the {@code HELM_HOME} 273 * environment variable, and bearing a name consisting of the 274 * supplied {@code name} suffixed with {@code -index.yaml} will be 275 * used instead 276 * 277 * @exception NullPointerException if either {@code name} or {@code 278 * uri} is {@code null} 279 * 280 * @exception IllegalArgumentException if {@code uri} is {@linkplain 281 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 282 * home" directory, or if {@code archiveCacheDirectory} is 283 * non-{@code null} and either empty or not {@linkplain 284 * Path#isAbsolute()} 285 * 286 * @see #ChartRepository(String, URI, Path, Path, Path) 287 * 288 * @see #getName() 289 * 290 * @see #getUri() 291 * 292 * @see #getCachedIndexPath() 293 */ 294 public ChartRepository(final String name, final URI uri, final Path archiveCacheDirectory, Path indexCacheDirectory, Path cachedIndexPath) { 295 super(); 296 Objects.requireNonNull(name); 297 Objects.requireNonNull(uri); 298 if (!uri.isAbsolute()) { 299 throw new IllegalArgumentException("!uri.isAbsolute(): " + uri); 300 } 301 302 Path helmHome = null; 303 304 if (archiveCacheDirectory == null) { 305 helmHome = getHelmHome(); 306 assert helmHome != null; 307 this.archiveCacheDirectory = helmHome.resolve("cache/archive"); 308 assert this.archiveCacheDirectory != null; 309 assert this.archiveCacheDirectory.isAbsolute(); 310 } else if (archiveCacheDirectory.toString().isEmpty()) { 311 throw new IllegalArgumentException("archiveCacheDirectory.toString().isEmpty(): " + archiveCacheDirectory); 312 } else if (!archiveCacheDirectory.isAbsolute()) { 313 throw new IllegalArgumentException("!archiveCacheDirectory.isAbsolute(): " + archiveCacheDirectory); 314 } else { 315 this.archiveCacheDirectory = archiveCacheDirectory; 316 } 317 assert this.archiveCacheDirectory != null; 318 assert this.archiveCacheDirectory.isAbsolute(); 319 if (!Files.isDirectory(this.archiveCacheDirectory)) { 320 throw new IllegalArgumentException("!Files.isDirectory(this.archiveCacheDirectory): " + this.archiveCacheDirectory); 321 } 322 323 if (cachedIndexPath == null || cachedIndexPath.toString().isEmpty()) { 324 cachedIndexPath = Paths.get(new StringBuilder(name).append("-index.yaml").toString()); 325 } 326 assert cachedIndexPath != null; 327 328 if (cachedIndexPath.isAbsolute()) { 329 this.indexCacheDirectory = null; 330 this.cachedIndexPath = cachedIndexPath; 331 } else { 332 if (indexCacheDirectory == null) { 333 if (helmHome == null) { 334 helmHome = getHelmHome(); 335 assert helmHome != null; 336 } 337 this.indexCacheDirectory = helmHome.resolve("repository/cache"); 338 assert this.indexCacheDirectory.isAbsolute(); 339 } else if (!indexCacheDirectory.isAbsolute()) { 340 throw new IllegalArgumentException("!indexCacheDirectory.isAbsolute(): " + indexCacheDirectory); 341 } else { 342 this.indexCacheDirectory = indexCacheDirectory; 343 assert this.indexCacheDirectory.isAbsolute(); 344 } 345 if (!Files.isDirectory(this.indexCacheDirectory)) { 346 throw new IllegalArgumentException("!Files.isDirectory(this.indexCacheDirectory): " + this.indexCacheDirectory); 347 } 348 this.cachedIndexPath = this.indexCacheDirectory.resolve(cachedIndexPath); 349 } 350 assert this.cachedIndexPath != null; 351 assert this.cachedIndexPath.isAbsolute(); 352 353 this.name = name; 354 this.uri = uri; 355 } 356 357 358 /* 359 * Instance methods. 360 */ 361 362 363 /** 364 * Returns the name of this {@link ChartRepository}. 365 * 366 * <p>This method never returns {@code null}.</p> 367 * 368 * @return the non-{@code null} name of this {@link ChartRepository} 369 */ 370 public final String getName() { 371 return this.name; 372 } 373 374 /** 375 * Returns the {@link URI} of the root of this {@link 376 * ChartRepository}. 377 * 378 * <p>This method never returns {@code null}.</p> 379 * 380 * <p>The {@link URI} returned by this method is guaranteed to be 381 * {@linkplain URI#isAbsolute() absolute}.</p> 382 * 383 * @return the non-{@code null}, {@linkplain URI#isAbsolute() 384 * absolute} {@link URI} of the root of this {@link ChartRepository} 385 */ 386 public final URI getUri() { 387 return this.uri; 388 } 389 390 /** 391 * Returns a non-{@code null}, {@linkplain Path#isAbsolute() 392 * absolute} {@link Path} to the file that contains or will contain 393 * a copy of the chart repository's <a 394 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 395 * index.yaml}</a> file. 396 * 397 * <p>This method never returns {@code null}.</p> 398 * 399 * @return a non-{@code null}, {@linkplain Path#isAbsolute() 400 * absolute} {@link Path} to the file that contains or will contain 401 * a copy of the chart repository's <a 402 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 403 * index.yaml}</a> file 404 */ 405 public final Path getCachedIndexPath() { 406 return this.cachedIndexPath; 407 } 408 409 /** 410 * Returns the {@link Index} for this {@link ChartRepository}. 411 * 412 * <p>This method never returns {@code null}.</p> 413 * 414 * <p>If this method has not been invoked before on this {@link 415 * ChartRepository}, then the {@linkplain #getCachedIndexPath() 416 * cached copy} of the chart repository's <a 417 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 418 * index.yaml}</a> file is parsed into an {@link Index} and that 419 * {@link Index} is stored in an instance variable before it is 420 * returned.</p> 421 * 422 * <p>If no {@linkplain #getCachedIndexPath() cached copy} of the 423 * chart repository's <a 424 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 425 * index.yaml}</a> file exists, then one is {@linkplain 426 * #downloadIndex() downloaded} first.</p> 427 * 428 * return the {@link Index} representing the contents of this {@link 429 * ChartRepository}; never {@code null} 430 * 431 * @exception IOException if there was a problem either parsing an 432 * <a 433 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 434 * index.yaml}</a> file or downloading it 435 * 436 * @exception URISyntaxException if one of the URIs in the <a 437 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 438 * index.yaml}</a> file is invalid 439 * 440 * @see #getIndex(boolean) 441 * 442 * @see #downloadIndex() 443 */ 444 public final Index getIndex() throws IOException, URISyntaxException { 445 return this.getIndex(false); 446 } 447 448 /** 449 * Returns the {@link Index} for this {@link ChartRepository}. 450 * 451 * <p>This method never returns {@code null}.</p> 452 * 453 * <p>If this method has not been invoked before on this {@link 454 * ChartRepository}, then the {@linkplain #getCachedIndexPath() 455 * cached copy} of the chart repository's <a 456 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 457 * index.yaml}</a> file is parsed into an {@link Index} and that 458 * {@link Index} is stored in an instance variable before it is 459 * returned.</p> 460 * 461 * <p>If the {@linkplain #getCachedIndexPath() cached copy} of the 462 * chart repository's <a 463 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 464 * index.yaml}</a> file {@linkplain #isCachedIndexExpired() has 465 * expired}, then one is {@linkplain #downloadIndex() downloaded} 466 * first.</p> 467 * 468 * @param forceDownload if {@code true} then no caching will happen 469 * 470 * @return the {@link Index} representing the contents of this {@link 471 * ChartRepository}; never {@code null} 472 * 473 * @exception IOException if there was a problem either parsing an 474 * <a 475 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 476 * index.yaml}</a> file or downloading it 477 * 478 * @exception URISyntaxException if one of the URIs in the <a 479 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 480 * index.yaml}</a> file is invalid 481 * 482 * @see #getIndex(boolean) 483 * 484 * @see #downloadIndex() 485 * 486 * @see #isCachedIndexExpired() 487 */ 488 public final Index getIndex(final boolean forceDownload) throws IOException, URISyntaxException { 489 if (forceDownload || this.index == null) { 490 final Path cachedIndexPath = this.getCachedIndexPath(); 491 assert cachedIndexPath != null; 492 if (forceDownload || this.isCachedIndexExpired()) { 493 this.downloadIndexTo(cachedIndexPath); 494 } 495 this.index = Index.loadFrom(cachedIndexPath); 496 assert this.index != null; 497 } 498 return this.index; 499 } 500 501 /** 502 * Returns {@code true} if the {@linkplain #getCachedIndexPath() 503 * cached copy} of the <a 504 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 505 * index.yaml}</a> file is to be considered stale. 506 * 507 * <p>The default implementation of this method returns the negation 508 * of the return value of an invocation of the {@link 509 * Files#isRegularFile(Path, LinkOption...)} method on the return value of the 510 * {@link #getCachedIndexPath()} method.</p> 511 * 512 * @return {@code true} if the {@linkplain #getCachedIndexPath() 513 * cached copy} of the <a 514 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 515 * index.yaml}</a> file is to be considered stale; {@code false} otherwise 516 * 517 * @see #getIndex(boolean) 518 */ 519 public boolean isCachedIndexExpired() { 520 final Path cachedIndexPath = this.getCachedIndexPath(); 521 assert cachedIndexPath != null; 522 return !Files.isRegularFile(cachedIndexPath); 523 } 524 525 /** 526 * Clears the {@link Index} stored internally by this {@link 527 * ChartRepository}, paving the way for a fresh copy to be installed 528 * by the {@link #getIndex(boolean)} method, and returns the old 529 * value. 530 * 531 * <p>This method may return {@code null} if {@code 532 * #getIndex(boolean)} has not yet been called.</p> 533 * 534 * @return the {@link Index}, or {@code null} 535 */ 536 public final Index clearIndex() { 537 final Index returnValue = this.index; 538 this.index = null; 539 return returnValue; 540 } 541 542 /** 543 * Invokes the {@link #downloadIndexTo(Path)} method with the return 544 * value of the {@link #getCachedIndexPath()} method. 545 * 546 * <p>This method never returns {@code null}.</p> 547 * 548 * @return {@link Path} the {@link Path} to which the {@code 549 * index.yaml} file was downloaded; never {@code null} 550 * 551 * @exception IOException if there was a problem downloading 552 * 553 * @see #downloadIndexTo(Path) 554 */ 555 public final Path downloadIndex() throws IOException { 556 return this.downloadIndexTo(this.getCachedIndexPath()); 557 } 558 559 /** 560 * Downloads a copy of the chart repository's <a 561 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 562 * index.yaml}</a> file to the {@link Path} specified and returns 563 * the canonical representation of the {@link Path} to which the 564 * file was actually downloaded. 565 * 566 * <p>This method never returns {@code null}.</p> 567 * 568 * <p>Overrides of this method must not return {@code null}.</p> 569 * 570 * <p>The default implementation of this method actually downloads 571 * the <a 572 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 573 * index.yaml}</a> file to a {@linkplain 574 * Files#createTempFile(String, String, FileAttribute...) temporary 575 * file} first, and then {@linkplain StandardCopyOption#ATOMIC_MOVE 576 * atomically renames it}.</p> 577 * 578 * @param path the {@link Path} to download the <a 579 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 580 * index.yaml}</a> file to; may be {@code null} in which case the 581 * return value of the {@link #getCachedIndexPath()} method will be 582 * used instead 583 * 584 * @return the {@link Path} to the file; never {@code null} 585 * 586 * @exception IOException if there was a problem downloading 587 */ 588 public Path downloadIndexTo(Path path) throws IOException { 589 final URI baseUri = this.getUri(); 590 if (baseUri == null) { 591 throw new IllegalStateException("getUri() == null"); 592 } 593 final URI indexUri = baseUri.resolve("index.yaml"); 594 assert indexUri != null; 595 final URL indexUrl = indexUri.toURL(); 596 assert indexUrl != null; 597 if (path == null) { 598 path = this.getCachedIndexPath(); 599 } 600 assert path != null; 601 if (!path.isAbsolute()) { 602 assert this.indexCacheDirectory != null; 603 assert this.indexCacheDirectory.isAbsolute(); 604 path = this.indexCacheDirectory.resolve(path); 605 assert path != null; 606 assert path.isAbsolute(); 607 } 608 final Path temporaryPath = Files.createTempFile(new StringBuilder(this.getName()).append("-index-").toString(), ".yaml"); 609 assert temporaryPath != null; 610 try (final BufferedInputStream stream = new BufferedInputStream(this.openStream(indexUrl))) { 611 Files.copy(stream, temporaryPath, StandardCopyOption.REPLACE_EXISTING); 612 } catch (final IOException throwMe) { 613 try { 614 Files.deleteIfExists(temporaryPath); 615 } catch (final IOException suppressMe) { 616 throwMe.addSuppressed(suppressMe); 617 } 618 throw throwMe; 619 } 620 return Files.move(temporaryPath, path, StandardCopyOption.ATOMIC_MOVE); 621 } 622 623 /** 624 * Creates a new {@link Index} from the contents of the {@linkplain 625 * #getCachedIndexPath() cached copy of the chart repository's 626 * <code>index.yaml</code> file} and returns it. 627 * 628 * <p>This method never returns {@code null}.</p> 629 * 630 * <p>Overrides of this method must not return {@code null}.</p> 631 * 632 * @return a new {@link Index}; never {@code null} 633 * 634 * @exception IOException if there was a problem reading the file 635 * 636 * @exception URISyntaxException if a URI in the file was invalid 637 * 638 * @see Index#loadFrom(Path) 639 */ 640 public Index loadIndex() throws IOException, URISyntaxException { 641 Path path = this.getCachedIndexPath(); 642 assert path != null; 643 if (!path.isAbsolute()) { 644 assert this.indexCacheDirectory != null; 645 assert this.indexCacheDirectory.isAbsolute(); 646 path = this.indexCacheDirectory.resolve(path); 647 assert path != null; 648 assert path.isAbsolute(); 649 } 650 return Index.loadFrom(path); 651 } 652 653 /** 654 * Given a Helm chart name and its version, returns the local {@link 655 * Path}, representing a local copy of the Helm chart as downloaded 656 * from the chart repository represented by this {@link 657 * ChartRepository}, downloading the archive if necessary. 658 * 659 * <p>This method may return {@code null}.</p> 660 * 661 * @param chartName the name of the chart whose local {@link Path} 662 * should be returned; must not be {@code null} 663 * 664 * @param chartVersion the version of the chart to select; may be 665 * {@code null} in which case "latest" semantics are implied 666 * 667 * @return the {@link Path} to the chart archive, or {@code null} 668 * 669 * @exception IOException if there was a problem downloading 670 * 671 * @exception URISyntaxException if this {@link ChartRepository}'s 672 * {@linkplain #getIndex() associated <code>Index</code>} could not 673 * be parsed 674 * 675 * @exception NullPointerException if {@code chartName} is {@code 676 * null} 677 */ 678 public final Path getCachedChartPath(final String chartName, String chartVersion) throws IOException, URISyntaxException { 679 Objects.requireNonNull(chartName); 680 Path returnValue = null; 681 if (chartVersion == null) { 682 final Index index = this.getIndex(false); 683 assert index != null; 684 final Index.Entry entry = index.getEntry(chartName, null /* latest */); 685 if (entry != null) { 686 chartVersion = entry.getVersion(); 687 } 688 } 689 if (chartVersion != null) { 690 assert this.archiveCacheDirectory != null; 691 final StringBuilder chartKey = new StringBuilder(chartName).append("-").append(chartVersion); 692 final String chartFilename = new StringBuilder(chartKey).append(".tgz").toString(); 693 final Path cachedChartPath = this.archiveCacheDirectory.resolve(chartFilename); 694 assert cachedChartPath != null; 695 if (!Files.isRegularFile(cachedChartPath)) { 696 final Index index = this.getIndex(true); 697 assert index != null; 698 final Index.Entry entry = index.getEntry(chartName, chartVersion); 699 if (entry != null) { 700 URI chartUri = entry.getFirstUri(); 701 if (chartUri != null) { 702 703 // See https://github.com/kubernetes/helm/issues/3057 704 if (!chartUri.isAbsolute()) { 705 final URI chartRepositoryUri = this.getUri(); 706 assert chartRepositoryUri != null; 707 assert chartRepositoryUri.isAbsolute(); 708 chartUri = chartRepositoryUri.resolve(chartUri); 709 assert chartUri != null; 710 assert chartUri.isAbsolute(); 711 } 712 713 final URL chartUrl = chartUri.toURL(); 714 assert chartUrl != null; 715 final Path temporaryPath = Files.createTempFile(chartKey.append("-").toString(), ".tgz"); 716 assert temporaryPath != null; 717 try (final InputStream stream = new BufferedInputStream(this.openStream(chartUrl))) { 718 Files.copy(stream, temporaryPath, StandardCopyOption.REPLACE_EXISTING); 719 } catch (final IOException throwMe) { 720 try { 721 Files.deleteIfExists(temporaryPath); 722 } catch (final IOException suppressMe) { 723 throwMe.addSuppressed(suppressMe); 724 } 725 throw throwMe; 726 } 727 Files.move(temporaryPath, cachedChartPath, StandardCopyOption.ATOMIC_MOVE); 728 } 729 } 730 } 731 returnValue = cachedChartPath; 732 } 733 return returnValue; 734 } 735 736 /** 737 * Returns an {@link InputStream} corresponding to the supplied 738 * {@link URL}. 739 * 740 * <p>This method may return {@code null}.</p> 741 * 742 * <p>Overrides of this method are permitted to return {@code 743 * null}.</p> 744 * 745 * @param url the {@link URL} whose affiliated {@link InputStream} 746 * should be returned; may be {@code null} in which case {@code 747 * null} will be returned 748 * 749 * @return an {@link InputStream} appropriate for the supplied 750 * {@link URL}, or {@code null} 751 * 752 * @exception IOException if an error occurs while connecting to the 753 * supplied {@link URL} 754 */ 755 protected InputStream openStream(final URL url) throws IOException { 756 InputStream returnValue = null; 757 if (url != null) { 758 final URLConnection urlConnection = url.openConnection(); 759 assert urlConnection != null; 760 urlConnection.setRequestProperty("User-Agent", "microbean-helm"); 761 returnValue = urlConnection.getInputStream(); 762 } 763 return returnValue; 764 } 765 766 /** 767 * {@inheritDoc} 768 * 769 * <p>This implementation calls the {@link 770 * #getCachedChartPath(String, String)} method with the supplied 771 * arguments and uses a {@link TapeArchiveChartLoader} to load the 772 * resulting archive into a {@link Chart.Builder} object.</p> 773 */ 774 @Override 775 public Chart.Builder resolve(final String chartName, String chartVersion) throws ChartResolverException { 776 Objects.requireNonNull(chartName); 777 Chart.Builder returnValue = null; 778 Path cachedChartPath = null; 779 try { 780 cachedChartPath = this.getCachedChartPath(chartName, chartVersion); 781 } catch (final IOException | URISyntaxException exception) { 782 throw new ChartResolverException(exception.getMessage(), exception); 783 } 784 if (cachedChartPath != null && Files.isRegularFile(cachedChartPath)) { 785 try (final TapeArchiveChartLoader loader = new TapeArchiveChartLoader()) { 786 returnValue = loader.load(new TarInputStream(new GZIPInputStream(new BufferedInputStream(Files.newInputStream(cachedChartPath))))); 787 } catch (final IOException exception) { 788 throw new ChartResolverException(exception.getMessage(), exception); 789 } 790 } 791 return returnValue; 792 } 793 794 /** 795 * Returns a {@link Path} representing "Helm home": the root 796 * directory for various Helm-related metadata as specified by 797 * either the {@code helm.home} system property or the {@code 798 * HELM_HOME} environment variable. 799 * 800 * <p>This method never returns {@code null}.</p> 801 * 802 * <p>No guarantee is made by this method regarding whether the 803 * returned {@link Path} actually denotes a directory.</p> 804 * 805 * @return a {@link Path} representing "Helm home"; never {@code 806 * null} 807 * 808 * @exception SecurityException if there are not sufficient 809 * permissions to read system properties or environment variables 810 */ 811 static final Path getHelmHome() { 812 String helmHome = System.getProperty("helm.home", System.getenv("HELM_HOME")); 813 if (helmHome == null) { 814 helmHome = Paths.get(System.getProperty("user.home")).resolve(".helm").toString(); 815 assert helmHome != null; 816 } 817 return Paths.get(helmHome); 818 } 819 820 821 /* 822 * Inner and nested classes. 823 */ 824 825 826 /** 827 * A class representing certain of the contents of a <a 828 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">Helm 829 * chart repository's {@code index.yaml} file</a>. 830 * 831 * @author <a href="https://about.me/lairdnelson" 832 * target="_parent">Laird Nelson</a> 833 */ 834 @Experimental 835 public static final class Index { 836 837 838 /* 839 * Instance fields. 840 */ 841 842 843 /** 844 * An {@linkplain Collections#unmodifiableSortedMap(SortedMap) 845 * immutable} {@link SortedMap} of {@link SortedSet}s of {@link 846 * Entry} objects whose values represent enough information to 847 * derive a URI to a Helm chart. 848 * 849 * <p>This field is never {@code null}.</p> 850 */ 851 private final SortedMap<String, SortedSet<Entry>> entries; 852 853 854 /* 855 * Constructors. 856 */ 857 858 859 /** 860 * Creates a new {@link Index}. 861 * 862 * @param entries a {@link Map} of {@link SortedSet}s of {@link 863 * Entry} objects indexed by the name of the Helm chart they 864 * describe; may be {@code null}; copied by value 865 */ 866 Index(final Map<? extends String, ? extends SortedSet<Entry>> entries) { 867 super(); 868 if (entries == null || entries.isEmpty()) { 869 this.entries = Collections.emptySortedMap(); 870 } else { 871 this.entries = Collections.unmodifiableSortedMap(deepCopy(entries)); 872 } 873 } 874 875 876 /* 877 * Instance methods. 878 */ 879 880 881 /** 882 * Creates and returns a new {@link Index} consisting of all this 883 * {@link Index} instance's {@linkplain #getEntries() entries} 884 * augmented with those entries from the supplied {@link Index} 885 * that this {@link Index} instance did not already contain. 886 * 887 * @param other the {@link Index} to merge in; may be {@code null} 888 * 889 * @return a new {@link Index} reflecting the merge operation 890 */ 891 @Experimental 892 public final Index merge(final Index other) { 893 final Index returnValue; 894 final Map<String, SortedSet<Entry>> myEntries = this.getEntries(); 895 final Map<String, SortedSet<Entry>> otherEntries; 896 if (other == null) { 897 otherEntries = null; 898 } else { 899 otherEntries = other.getEntries(); 900 } 901 if (otherEntries == null || otherEntries.isEmpty()) { 902 if (myEntries == null || myEntries.isEmpty()) { 903 returnValue = new Index(null); 904 } else { 905 returnValue = new Index(myEntries); 906 } 907 } else if (myEntries == null || myEntries.isEmpty()) { 908 returnValue = new Index(otherEntries); 909 } else { 910 final Map<String, SortedSet<Entry>> mergedEntries = deepCopy(myEntries); 911 final Set<Map.Entry<String, SortedSet<Entry>>> otherEntrySet = otherEntries.entrySet(); 912 if (otherEntrySet != null && !otherEntrySet.isEmpty()) { 913 for (final Map.Entry<? extends String, ? extends SortedSet<Entry>> otherEntrySetElement : otherEntrySet) { 914 if (otherEntrySetElement != null) { 915 final SortedSet<Entry> otherValues = otherEntrySetElement.getValue(); 916 if (otherValues != null && !otherValues.isEmpty()) { 917 for (final Entry otherEntry : otherValues) { 918 if (otherEntry != null) { 919 final String otherEntryName = otherEntry.getName(); 920 final Entry myCorrespondingEntry = this.getEntry(otherEntryName, otherEntry.getVersion()); 921 if (myCorrespondingEntry == null) { 922 SortedSet<Entry> myRelatedEntries = mergedEntries.get(otherEntryName); 923 if (myRelatedEntries == null) { 924 myRelatedEntries = new TreeSet<>(Collections.reverseOrder()); 925 mergedEntries.put(otherEntryName, myRelatedEntries); 926 } 927 assert !myRelatedEntries.contains(otherEntry); 928 myRelatedEntries.add(otherEntry); 929 } 930 } 931 } 932 } 933 } 934 } 935 } 936 returnValue = new Index(mergedEntries); 937 } 938 return returnValue; 939 } 940 941 /** 942 * Returns a non-{@code null}, {@linkplain 943 * Collections#unmodifiableMap(Map) immutable} {@link Map} of 944 * {@link SortedSet}s of {@link Entry} objects, indexed by the 945 * name of the Helm chart they describe. 946 * 947 * @return a non-{@code null}, {@linkplain 948 * Collections#unmodifiableMap(Map) immutable} {@link Map} of 949 * {@link SortedSet}s of {@link Entry} objects, indexed by the 950 * name of the Helm chart they describe 951 */ 952 public final Map<String, SortedSet<Entry>> getEntries() { 953 return this.entries; 954 } 955 956 /** 957 * Returns an {@link Entry} identified by the supplied {@code 958 * name} and {@code version}, if there is one. 959 * 960 * <p>This method may return {@code null}.</p> 961 * 962 * @param name the name of the Helm chart whose related {@link 963 * Entry} is desired; must not be {@code null} 964 * 965 * @param versionString the version of the Helm chart whose 966 * related {@link Entry} is desired; may be {@code null} in which 967 * case "latest" semantics are implied 968 * 969 * @return an {@link Entry}, or {@code null} 970 * 971 * @exception NullPointerException if {@code name} is {@code null} 972 */ 973 public final Entry getEntry(final String name, final String versionString) { 974 Objects.requireNonNull(name); 975 Entry returnValue = null; 976 final Map<String, SortedSet<Entry>> entries = this.getEntries(); 977 if (entries != null && !entries.isEmpty()) { 978 final SortedSet<Entry> entrySet = entries.get(name); 979 if (entrySet != null && !entrySet.isEmpty()) { 980 if (versionString == null) { 981 returnValue = entrySet.first(); 982 } else { 983 for (final Entry entry : entrySet) { 984 // XXX TODO FIXME: probably want to make this a 985 // constraint match, not just an equality comparison 986 if (entry != null && versionString.equals(entry.getVersion())) { 987 returnValue = entry; 988 break; 989 } 990 } 991 } 992 } 993 } 994 return returnValue; 995 } 996 997 998 /* 999 * Static methods. 1000 */ 1001 1002 1003 /** 1004 * Creates a new {@link Index} whose contents are sourced from the 1005 * YAML file located at the supplied {@link Path}. 1006 * 1007 * <p>This method never returns {@code null}.</p> 1008 * 1009 * @param path the {@link Path} to a YAML file whose contents are 1010 * those of a <a 1011 * href="https://docs.helm.sh/developing_charts/#the-index-file">Helm 1012 * chart repository index</a>; must not be {@code null} 1013 * 1014 * @return a new {@link Index}; never {@code null} 1015 * 1016 * @exception IOException if there was a problem reading the file 1017 * 1018 * @exception URISyntaxException if one of the URIs in the file 1019 * was invalid 1020 * 1021 * @exception NullPointerException if {@code path} is {@code null} 1022 * 1023 * @see #loadFrom(InputStream) 1024 */ 1025 public static final Index loadFrom(final Path path) throws IOException, URISyntaxException { 1026 Objects.requireNonNull(path); 1027 final Index returnValue; 1028 try (final BufferedInputStream stream = new BufferedInputStream(Files.newInputStream(path))) { 1029 returnValue = loadFrom(stream); 1030 } 1031 return returnValue; 1032 } 1033 1034 /** 1035 * Creates a new {@link Index} whose contents are sourced from the 1036 * <a 1037 * href="https://docs.helm.sh/developing_charts/#the-index-file">Helm 1038 * chart repository index</a> YAML contents represented by the 1039 * supplied {@link InputStream}. 1040 * 1041 * <p>This method never returns {@code null}.</p> 1042 * 1043 * @param stream the {@link InputStream} to a YAML file whose contents are 1044 * those of a <a 1045 * href="https://docs.helm.sh/developing_charts/#the-index-file">Helm 1046 * chart repository index</a>; must not be {@code null} 1047 * 1048 * @return a new {@link Index}; never {@code null} 1049 * 1050 * @exception IOException if there was a problem reading the file 1051 * 1052 * @exception URISyntaxException if one of the URIs in the file 1053 * was invalid 1054 * 1055 * @exception NullPointerException if {@code path} is {@code null} 1056 */ 1057 public static final Index loadFrom(final InputStream stream) throws IOException, URISyntaxException { 1058 Objects.requireNonNull(stream); 1059 final Index returnValue; 1060 final Map<?, ?> yamlMap = new Yaml().loadAs(stream, Map.class); 1061 if (yamlMap == null || yamlMap.isEmpty()) { 1062 returnValue = new Index(null); 1063 } else { 1064 final SortedMap<String, SortedSet<Index.Entry>> sortedEntryMap = new TreeMap<>(); 1065 @SuppressWarnings("unchecked") 1066 final Map<? extends String, ? extends Collection<? extends Map<?, ?>>> entriesMap = (Map<? extends String, ? extends Collection<? extends Map<?, ?>>>)yamlMap.get("entries"); 1067 if (entriesMap != null && !entriesMap.isEmpty()) { 1068 final Collection<? extends Map.Entry<? extends String, ? extends Collection<? extends Map<?, ?>>>> entries = entriesMap.entrySet(); 1069 if (entries != null && !entries.isEmpty()) { 1070 for (final Map.Entry<? extends String, ? extends Collection<? extends Map<?, ?>>> mapEntry : entries) { 1071 if (mapEntry != null) { 1072 final String entryName = mapEntry.getKey(); 1073 if (entryName != null) { 1074 final Collection<? extends Map<?, ?>> entryContents = mapEntry.getValue(); 1075 if (entryContents != null && !entryContents.isEmpty()) { 1076 for (final Map<?, ?> entryMap : entryContents) { 1077 if (entryMap != null && !entryMap.isEmpty()) { 1078 final Metadata.Builder metadataBuilder = Metadata.newBuilder(); 1079 assert metadataBuilder != null; 1080 Metadatas.populateMetadataBuilder(metadataBuilder, entryMap); 1081 @SuppressWarnings("unchecked") 1082 final Collection<? extends String> uriStrings = (Collection<? extends String>)entryMap.get("urls"); 1083 Set<URI> uris = new LinkedHashSet<>(); 1084 if (uriStrings != null && !uriStrings.isEmpty()) { 1085 for (final String uriString : uriStrings) { 1086 if (uriString != null && !uriString.isEmpty()) { 1087 uris.add(new URI(uriString)); 1088 } 1089 } 1090 } 1091 final String digest = (String)entryMap.get("digest"); 1092 SortedSet<Index.Entry> entryObjects = sortedEntryMap.get(entryName); 1093 if (entryObjects == null) { 1094 entryObjects = new TreeSet<>(Collections.reverseOrder()); 1095 sortedEntryMap.put(entryName, entryObjects); 1096 } 1097 entryObjects.add(new Index.Entry(metadataBuilder, uris, digest)); 1098 } 1099 } 1100 } 1101 } 1102 } 1103 } 1104 } 1105 } 1106 returnValue = new Index(sortedEntryMap); 1107 } 1108 return returnValue; 1109 } 1110 1111 /** 1112 * Performs a deep copy of the supplied {@link Map} such that the 1113 * {@link SortedMap} returned has copies of the supplied {@link 1114 * Map}'s {@linkplain Map#values() values}. 1115 * 1116 * <p>This method may return {@code null} if {@code source} is 1117 * {@code null}.</p> 1118 * 1119 * <p>The {@link SortedMap} returned by this method is 1120 * mutable.</p> 1121 * 1122 * @param source the {@link Map} to copy; may be {@code null} in 1123 * which case {@code null} will be returned 1124 * 1125 * @return a mutable {@link SortedMap}, or {@code null} 1126 */ 1127 private static final SortedMap<String, SortedSet<Entry>> deepCopy(final Map<? extends String, ? extends SortedSet<Entry>> source) { 1128 final SortedMap<String, SortedSet<Entry>> returnValue; 1129 if (source == null) { 1130 returnValue = null; 1131 } else if (source.isEmpty()) { 1132 returnValue = Collections.emptySortedMap(); 1133 } else { 1134 returnValue = new TreeMap<>(); 1135 final Collection<? extends Map.Entry<? extends String, ? extends SortedSet<Entry>>> entrySet = source.entrySet(); 1136 if (entrySet != null && !entrySet.isEmpty()) { 1137 for (final Map.Entry<? extends String, ? extends SortedSet<Entry>> entry : entrySet) { 1138 final String key = entry.getKey(); 1139 final SortedSet<Entry> value = entry.getValue(); 1140 if (value == null) { 1141 returnValue.put(key, null); 1142 } else { 1143 final SortedSet<Entry> newValue = new TreeSet<>(value.comparator()); 1144 newValue.addAll(value); 1145 returnValue.put(key, newValue); 1146 } 1147 } 1148 } 1149 } 1150 return returnValue; 1151 } 1152 1153 1154 /* 1155 * Inner and nested classes. 1156 */ 1157 1158 1159 /** 1160 * An entry in a <a 1161 * href="https://docs.helm.sh/developing_charts/#the-index-file">Helm 1162 * chart repository index</a>. 1163 * 1164 * @author <a href="https://about.me/lairdnelson" 1165 * target="_parent">Laird Nelson</a> 1166 */ 1167 @Experimental 1168 public static final class Entry implements Comparable<Entry> { 1169 1170 1171 /* 1172 * Instance fields. 1173 */ 1174 1175 1176 /** 1177 * A {@link MetadataOrBuilder} representing most of the contents 1178 * of the entry. 1179 * 1180 * <p>This field is never {@code null}.</p> 1181 */ 1182 private final MetadataOrBuilder metadata; 1183 1184 /** 1185 * An {@linkplain Collections#unmodifiableSet(Set) immutable} 1186 * {@link Set} of {@link URI}s describing where the particular 1187 * Helm chart described by this {@link Entry} may be downloaded 1188 * from. 1189 * 1190 * <p>This field is never {@code null}.</p> 1191 */ 1192 private final Set<URI> uris; 1193 1194 1195 private final String digest; 1196 1197 /* 1198 * Constructors. 1199 */ 1200 1201 1202 /** 1203 * Creates a new {@link Entry}. 1204 * 1205 * @param metadata a {@link MetadataOrBuilder} representing most 1206 * of the contents of the entry; must not be {@code null} 1207 * 1208 * @param uris a {@link Collection} of {@link URI}s describing 1209 * where the particular Helm chart described by this {@link 1210 * Entry} may be downloaded from; may be {@code null}; copied by 1211 * value 1212 * 1213 * @exception NullPointerException if {@code metadata} is {@code 1214 * null} 1215 * 1216 * @see #Entry(MetadataOrBuilder, Collection, String) 1217 */ 1218 Entry(final MetadataOrBuilder metadata, final Collection<? extends URI> uris) { 1219 this(metadata, uris, null); 1220 } 1221 1222 /** 1223 * Creates a new {@link Entry}. 1224 * 1225 * @param metadata a {@link MetadataOrBuilder} representing most 1226 * of the contents of the entry; must not be {@code null} 1227 * 1228 * @param uris a {@link Collection} of {@link URI}s describing 1229 * where the particular Helm chart described by this {@link 1230 * Entry} may be downloaded from; may be {@code null}; copied by 1231 * value 1232 * 1233 * @param digest a SHA-256 message digest to be associated with 1234 * this {@link Entry}; may be {@code null} 1235 * 1236 * @exception NullPointerException if {@code metadata} is {@code 1237 * null} 1238 */ 1239 Entry(final MetadataOrBuilder metadata, final Collection<? extends URI> uris, final String digest) { 1240 super(); 1241 this.metadata = Objects.requireNonNull(metadata); 1242 if (uris == null || uris.isEmpty()) { 1243 this.uris = Collections.emptySet(); 1244 } else { 1245 this.uris = new LinkedHashSet<>(uris); 1246 } 1247 this.digest = digest; 1248 } 1249 1250 1251 /* 1252 * Instance methods. 1253 */ 1254 1255 1256 /** 1257 * Compares this {@link Entry} to the supplied {@link Entry} and 1258 * returns a value less than {@code 0} if this {@link Entry} is 1259 * "less than" the supplied {@link Entry}, {@code 1} if this 1260 * {@link Entry} is "greater than" the supplied {@link Entry} 1261 * and {@code 0} if this {@link Entry} is equal to the supplied 1262 * {@link Entry}. 1263 * 1264 * <p>{@link Entry} objects are compared by {@linkplain 1265 * #getName() name} first, then {@linkplain #getVersion() 1266 * version}.</p> 1267 * 1268 * <p>It is intended that this {@link 1269 * #compareTo(ChartRepository.Index.Entry)} method is 1270 * {@linkplain Comparable consistent with equals}.</p> 1271 * 1272 * @param her the {@link Entry} to compare; must not be {@code null} 1273 * 1274 * @return a value less than {@code 0} if this {@link Entry} is 1275 * "less than" the supplied {@link Entry}, {@code 1} if this 1276 * {@link Entry} is "greater than" the supplied {@link Entry} 1277 * and {@code 0} if this {@link Entry} is equal to the supplied 1278 * {@link Entry} 1279 * 1280 * @exception NullPointerException if the supplied {@link Entry} 1281 * is {@code null} 1282 */ 1283 @Override 1284 public final int compareTo(final Entry her) { 1285 Objects.requireNonNull(her); // see Comparable documentation 1286 1287 final String myName = this.getName(); 1288 final String herName = her.getName(); 1289 if (myName == null) { 1290 if (herName != null) { 1291 return -1; 1292 } 1293 } else if (herName == null) { 1294 return 1; 1295 } else { 1296 final int nameComparison = myName.compareTo(herName); 1297 if (nameComparison != 0) { 1298 return nameComparison; 1299 } 1300 } 1301 1302 final String myVersionString = this.getVersion(); 1303 final String herVersionString = her.getVersion(); 1304 if (myVersionString == null) { 1305 if (herVersionString != null) { 1306 return -1; 1307 } 1308 } else if (herVersionString == null) { 1309 return 1; 1310 } else { 1311 Version myVersion = null; 1312 try { 1313 myVersion = Version.valueOf(myVersionString); 1314 } catch (final IllegalArgumentException | ParseException badVersion) { 1315 myVersion = null; 1316 } 1317 Version herVersion = null; 1318 try { 1319 herVersion = Version.valueOf(herVersionString); 1320 } catch (final IllegalArgumentException | ParseException badVersion) { 1321 herVersion = null; 1322 } 1323 if (myVersion == null) { 1324 if (herVersion != null) { 1325 return -1; 1326 } 1327 } else if (herVersion == null) { 1328 return 1; 1329 } else { 1330 return myVersion.compareTo(herVersion); 1331 } 1332 } 1333 1334 return 0; 1335 } 1336 1337 /** 1338 * Returns a hashcode for this {@link Entry} based off its 1339 * {@linkplain #getName() name} and {@linkplain #getVersion() 1340 * version}. 1341 * 1342 * @return a hashcode for this {@link Entry} 1343 * 1344 * @see #compareTo(ChartRepository.Index.Entry) 1345 * 1346 * @see #equals(Object) 1347 * 1348 * @see #getName() 1349 * 1350 * @see #getVersion() 1351 */ 1352 @Override 1353 public final int hashCode() { 1354 int hashCode = 17; 1355 1356 final Object name = this.getName(); 1357 int c = name == null ? 0 : name.hashCode(); 1358 hashCode = 37 * hashCode + c; 1359 1360 final Object version = this.getVersion(); 1361 c = version == null ? 0 : version.hashCode(); 1362 hashCode = 37 * hashCode + c; 1363 1364 return hashCode; 1365 } 1366 1367 /** 1368 * Returns {@code true} if the supplied {@link Object} is an 1369 * {@link Entry} and has a {@linkplain #getName() name} and 1370 * {@linkplain #getVersion() version} equal to those of this 1371 * {@link Entry}. 1372 * 1373 * @param other the {@link Object} to test; may be {@code null} 1374 * in which case {@code false} will be returned 1375 * 1376 * @return {@code true} if this {@link Entry} is equal to the 1377 * supplied {@link Object}; {@code false} otherwise 1378 * 1379 * @see #compareTo(ChartRepository.Index.Entry) 1380 * 1381 * @see #getName() 1382 * 1383 * @see #getVersion() 1384 * 1385 * @see #hashCode() 1386 */ 1387 @Override 1388 public final boolean equals(final Object other) { 1389 if (other == this) { 1390 return true; 1391 } else if (other instanceof Entry) { 1392 final Entry her = (Entry)other; 1393 1394 final Object myName = this.getName(); 1395 if (myName == null) { 1396 if (her.getName() != null) { 1397 return false; 1398 } 1399 } else if (!myName.equals(her.getName())) { 1400 return false; 1401 } 1402 1403 final Object myVersion = this.getVersion(); 1404 if (myVersion == null) { 1405 if (her.getVersion() != null) { 1406 return false; 1407 } 1408 } else if (!myVersion.equals(her.getVersion())) { 1409 return false; 1410 } 1411 1412 return true; 1413 } else { 1414 return false; 1415 } 1416 } 1417 1418 /** 1419 * Returns the {@link MetadataOrBuilder} that comprises most of 1420 * the contents of this {@link Entry}. 1421 * 1422 * <p>This method never returns {@code null}.</p> 1423 * 1424 * @return the {@link MetadataOrBuilder} that comprises most of 1425 * the contents of this {@link Entry}; never {@code null} 1426 */ 1427 public final MetadataOrBuilder getMetadataOrBuilder() { 1428 return this.metadata; 1429 } 1430 1431 /** 1432 * Returns the return value of invoking the {@link 1433 * MetadataOrBuilder#getName()} method on the {@link 1434 * MetadataOrBuilder} returned by this {@link Entry}'s {@link 1435 * #getMetadataOrBuilder()} method. 1436 * 1437 * <p>This method may return {@code null}.</p> 1438 * 1439 * @return this {@link Entry}'s name, or {@code null} 1440 * 1441 * @see MetadataOrBuilder#getName() 1442 */ 1443 public final String getName() { 1444 final MetadataOrBuilder metadata = this.getMetadataOrBuilder(); 1445 assert metadata != null; 1446 return metadata.getName(); 1447 } 1448 1449 /** 1450 * Returns the return value of invoking the {@link 1451 * MetadataOrBuilder#getVersion()} method on the {@link 1452 * MetadataOrBuilder} returned by this {@link Entry}'s {@link 1453 * #getMetadataOrBuilder()} method. 1454 * 1455 * <p>This method may return {@code null}.</p> 1456 * 1457 * @return this {@link Entry}'s version, or {@code null} 1458 * 1459 * @see MetadataOrBuilder#getVersion() 1460 */ 1461 public final String getVersion() { 1462 final MetadataOrBuilder metadata = this.getMetadataOrBuilder(); 1463 assert metadata != null; 1464 return metadata.getVersion(); 1465 } 1466 1467 /** 1468 * Returns a non-{@code null}, {@linkplain 1469 * Collections#unmodifiableSet(Set) immutable} {@link Set} of 1470 * {@link URI}s representing the URIs from which the Helm chart 1471 * described by this {@link Entry} may be downloaded. 1472 * 1473 * <p>This method never returns {@code null}.</p> 1474 * 1475 * @return a non-{@code null}, {@linkplain 1476 * Collections#unmodifiableSet(Set) immutable} {@link Set} of 1477 * {@link URI}s representing the URIs from which the Helm chart 1478 * described by this {@link Entry} may be downloaded 1479 * 1480 * @see #getFirstUri() 1481 */ 1482 public final Set<URI> getUris() { 1483 return this.uris; 1484 } 1485 1486 /** 1487 * A convenience method that returns the first {@link URI} in 1488 * the {@link Set} of {@link URI}s returned by the {@link 1489 * #getUris()} method. 1490 * 1491 * <p>This method may return {@code null}.</p> 1492 * 1493 * @return the {@linkplain SortedSet#first() first} {@link URI} 1494 * in the {@link Set} of {@link URI}s returned by the {@link 1495 * #getUris()} method, or {@code null} 1496 * 1497 * @see #getUris() 1498 */ 1499 public final URI getFirstUri() { 1500 final Set<URI> uris = this.getUris(); 1501 final URI returnValue; 1502 if (uris == null || uris.isEmpty()) { 1503 returnValue = null; 1504 } else { 1505 final Iterator<URI> iterator = uris.iterator(); 1506 if (iterator == null || !iterator.hasNext()) { 1507 returnValue = null; 1508 } else { 1509 returnValue = iterator.next(); 1510 } 1511 } 1512 return returnValue; 1513 } 1514 1515 /** 1516 * Returns the SHA-256 message digest, in hexadecimal-encoded 1517 * {@link String} form, associated with this {@link Entry}. 1518 * 1519 * <p>This method may return {@code null}.</p> 1520 * 1521 * @return the SHA-256 message digest, in hexadecimal-encoded 1522 * {@link String} form, associated with this {@link Entry}, or 1523 * {@code null} 1524 */ 1525 public final String getDigest() { 1526 return this.digest; 1527 } 1528 1529 /** 1530 * Returns a non-{@code null} {@link String} representation of 1531 * this {@link Entry}. 1532 * 1533 * @return a non-{@code null} {@link String} representation of 1534 * this {@link Entry} 1535 */ 1536 @Override 1537 public final String toString() { 1538 String name = this.getName(); 1539 if (name == null || name.isEmpty()) { 1540 name = "unnamed"; 1541 } 1542 return new StringBuilder(name).append(" ").append(this.getVersion()).toString(); 1543 } 1544 1545 /** 1546 * Computes a SHA-256 message digest of the bytes readable from 1547 * the supplied {@link InputStream} and returns the result of 1548 * {@linkplain DatatypeConverter#printHexBinary(byte[]) 1549 * hexadecimal-encoding it}. 1550 * 1551 * <p>This method never returns {@code null}.</p> 1552 * 1553 * @param inputStream the {@link InputStream} to read from; must 1554 * not be {@code null} 1555 * 1556 * @return a {@linkplain 1557 * DatatypeConverter#printHexBinary(byte[]) hexadecimal-encoded} 1558 * SHA-256 message digest; never {@code null} 1559 * 1560 * @exception NullPointerException if {@code inputStream} is 1561 * {@code null} 1562 * 1563 * @exception IOException if an input or output error occurs 1564 */ 1565 @Experimental 1566 public static final String getDigest(final InputStream inputStream) throws IOException { 1567 Objects.requireNonNull(inputStream); 1568 MessageDigest md = null; 1569 try { 1570 md = MessageDigest.getInstance("SHA-256"); 1571 } catch (final NoSuchAlgorithmException noSuchAlgorithmException) { 1572 // SHA-256 is guaranteed to exist. 1573 throw new InternalError(noSuchAlgorithmException); 1574 } 1575 assert md != null; 1576 final ByteBuffer buffer = toByteBuffer(inputStream); 1577 assert buffer != null; 1578 md.update(buffer); 1579 return DatatypeConverter.printHexBinary(md.digest()); 1580 } 1581 1582 /** 1583 * Returns a {@link ByteBuffer} representing the supplied {@link 1584 * InputStream}. 1585 * 1586 * <p>This method never returns {@code null}.</p> 1587 * 1588 * @param stream the {@link InputStream} to represent; may be 1589 * {@code null} 1590 * 1591 * @return a non-{@code null} {@link ByteBuffer} 1592 * 1593 * @exception IOException if an input or output error occurs 1594 */ 1595 private static final ByteBuffer toByteBuffer(final InputStream stream) throws IOException { 1596 return ByteBuffer.wrap(read(stream)); 1597 } 1598 1599 /** 1600 * Fully reads the supplied {@link InputStream} into a {@code 1601 * byte} array and returns it. 1602 * 1603 * <p>This method never returns {@code null}.</p> 1604 * 1605 * @param stream the {@link InputStream} to read; may be {@code 1606 * null} 1607 * 1608 * @return a non-{@code null} {@code byte} array containing the 1609 * readable contents of the supplied {@link InputStream} 1610 * 1611 * @exception IOException if an input or output error occurs 1612 */ 1613 private static final byte[] read(final InputStream stream) throws IOException { 1614 byte[] returnValue = null; 1615 if (stream == null) { 1616 returnValue = new byte[0]; 1617 } else { 1618 try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { 1619 int bytesRead; 1620 final byte[] byteArray = new byte[4096]; 1621 while ((bytesRead = stream.read(byteArray, 0, byteArray.length)) != -1) { 1622 buffer.write(byteArray, 0, bytesRead); 1623 } 1624 buffer.flush(); 1625 returnValue = buffer.toByteArray(); 1626 } 1627 } 1628 return returnValue; 1629 } 1630 1631 1632 } 1633 1634 } 1635 1636}