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