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