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