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