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