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