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