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