001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2017 MicroBean.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.helm.chart;
018
019import java.io.IOException;
020import java.io.InputStream;
021
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.Iterator;
026import java.util.Map;
027import java.util.Map.Entry;
028import java.util.NavigableMap;
029import java.util.NavigableSet;
030import java.util.Objects;
031import java.util.TreeMap;
032import java.util.TreeSet;
033
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036
037import java.util.zip.GZIPInputStream;
038
039import com.google.protobuf.Any;
040import com.google.protobuf.ByteString;
041
042import hapi.chart.ChartOuterClass.Chart;
043import hapi.chart.ConfigOuterClass.Config;
044import hapi.chart.MetadataOuterClass.Maintainer;
045import hapi.chart.MetadataOuterClass.Metadata;
046import hapi.chart.TemplateOuterClass.Template;
047
048import org.kamranzafar.jtar.TarInputStream;
049
050import org.yaml.snakeyaml.Yaml;
051
052/**
053 * A partial {@link AbstractChartLoader} implementation that is capable of
054 * loading a Helm-compatible chart from any source that is {@linkplain
055 * #toNamedInputStreamEntries(Object) convertible into an
056 * <code>Iterable</code> of <code>InputStream</code>s indexed by their
057 * name}.
058 *
059 * @param <T> the type of source from which this {@link
060 * StreamOrientedChartLoader} is capable of loading Helm charts
061 *
062 * @author <a href="https://about.me/lairdnelson"
063 * target="_parent">Laird Nelson</a>
064 *
065 * @see #toNamedInputStreamEntries(Object)
066 */
067public abstract class StreamOrientedChartLoader<T> extends AbstractChartLoader<T> {
068
069
070  /*
071   * Static fields.
072   */
073
074  
075  /**
076   * A {@link Pattern} that matches the trailing component of a file
077   * name in a valid Helm chart structure, provided it is not preceded
078   * in its path components by either {@code /templates/} or {@code
079   * /charts/}, and stores it as capturing group {@code 1}.
080   *
081   * <h2>Examples</h2>
082   *
083   * <ul>
084   *
085   * <li>Given {@code wordpress/README.md}, yields {@code
086   * README.md}.</li>
087   *
088   * <li>Given {@code wordpress/charts/mariadb/README.md}, yields
089   * nothing.</li>
090   *
091   * <li>Given {@code wordpress/templates/deployment.yaml}, yields
092   * nothing.</li>
093   *
094   * <li>Given {@code wordpress/subdirectory/file.txt}, yields {@code
095   * subdirectory/file.txt}.</li>
096   *
097   * </ul>
098   */
099  private static final Pattern fileNamePattern = Pattern.compile("^/*[^/]+(?!.*/(?:charts|templates)/)/(.+)$");
100  
101  private static final Pattern templateFileNamePattern = Pattern.compile("^.+/(templates/[^/]+)$");
102
103  private static final Pattern subchartFileNamePattern = Pattern.compile("^.+/charts/[^._][^/]+/(.+)$");
104
105  /**
106   * <p>Please note that the lack of anchors ({@code ^} or {@code $})
107   * and the leading "{@code .*?}" in this pattern's {@linkplain
108   * Pattern#toString() value} are deliberate choices.</p>
109   */
110  private static final Pattern nonGreedySubchartsPattern = Pattern.compile(".*?/charts/[^/]+");
111
112  private static final Pattern chartNamePattern = Pattern.compile("^.+/charts/([^/]+).*$");
113
114  private static final Pattern basenamePattern = Pattern.compile("^.*/?(.*)$");
115
116
117  /*
118   * Constructors.
119   */
120
121
122  /**
123   * Creates a new {@link StreamOrientedChartLoader}.
124   */
125  protected StreamOrientedChartLoader() {
126    super();
127  }
128
129
130  /*
131   * Instance methods.
132   */
133
134
135  /**
136   * Converts the supplied {@code source} into an {@link Iterable} of
137   * {@link Entry} instances whose {@linkplain Entry#getKey() keys}
138   * are names and whose {@linkplain Entry#getValue() values} are
139   * corresponding {@link InputStream}s.
140   *
141   * <p>Implementations of this method must not return {@code
142   * null}.</p>
143   *
144   * @param source the source to convert; must not be {@code null}
145   *
146   * @return an {@link Iterable} of suitable {@link Entry} instances;
147   * never {@code null}
148   *
149   * @exception NullPointerException if {@code source} is {@code null}
150   *
151   * @exception IOException if an error occurs while converting
152   */
153  protected abstract Iterable<? extends Entry<? extends String, ? extends InputStream>> toNamedInputStreamEntries(final T source) throws IOException;
154
155  /**
156   * Creates a new {@link Chart} from the supplied {@code source} in
157   * some manner and returns it.
158   *
159   * <p>This method never returns {@code null}.
160   *
161   * <p>This method calls the {@link
162   * #load(hapi.chart.ChartOuterClass.Chart.Builder, Iterable)} method
163   * with the return value of the {@link
164   * #toNamedInputStreamEntries(Object)} method.</p>
165   *
166   * @param source the source object from which to load a new {@link
167   * Chart}; must not be {@code null}
168   *
169   * @return a new {@link Chart}; never {@code null}
170   *
171   * @exception NullPointerException if {@code source} is {@code null}
172   *
173   * @exception IllegalStateException if the {@link
174   * #load(hapi.chart.ChartOuterClass.Chart.Builder, Iterable)} method
175   * returns {@code null}
176   *
177   * @exception IOException if a problem is encountered while creating
178   * the {@link Chart} to return
179   *
180   * @see #toNamedInputStreamEntries(Object)
181   *
182   * @see #load(hapi.chart.ChartOuterClass.Chart.Builder, Iterable)
183   */
184  @Override
185  public Chart.Builder load(final Chart.Builder parent, final T source) throws IOException {
186    Objects.requireNonNull(source);
187    final Chart.Builder returnValue = this.load(parent, toNamedInputStreamEntries(source));
188    if (returnValue == null) {
189      throw new IllegalStateException("load(toNamedInputStreamEntries(source)) == null; source: " + source);
190    }
191    return returnValue;
192  }
193
194  /**
195   * Creates a new {@link Chart} from the supplied notional set of
196   * named {@link InputStream}s and returns it.
197   *
198   * <p>This method never returns {@code null}.
199   *
200   * <p>This method is called by the {@link #load(Object)} method.</p>
201   *
202   * @param entrySet the {@link Iterable} of {@link Entry} instances
203   * normally returned by the {@link
204   * #toNamedInputStreamEntries(Object)} method; must not be {@code
205   * null}
206   *
207   * @return a new {@link Chart}; never {@code null}
208   *
209   * @exception NullPointerException if {@code entrySet} is {@code
210   * null}
211   *
212   * @exception IOException if a problem is encountered while creating
213   * the {@link Chart} to return
214   *
215   * @see #toNamedInputStreamEntries(Object)
216   *
217   * @see #load(Object)
218   */
219  public Chart.Builder load(final Chart.Builder parent, final Iterable<? extends Entry<? extends String, ? extends InputStream>> entrySet) throws IOException {
220    Objects.requireNonNull(entrySet);
221    final Chart.Builder rootBuilder;
222    if (parent == null) {
223      rootBuilder = Chart.newBuilder();
224    } else {
225      rootBuilder = parent;
226    }
227    assert rootBuilder != null;
228    final NavigableMap<String, Chart.Builder> chartBuilders = new TreeMap<>(new ChartPathComparator());
229    // XXX TODO FIXME: do we really want to say the root is null?
230    // Or should it always be a path named after the chart?
231    chartBuilders.put(null, rootBuilder);
232    for (final Entry<? extends String, ? extends InputStream> entry : entrySet) {
233      if (entry != null) {
234        this.addFile(chartBuilders, entry.getKey(), entry.getValue());
235      }
236    }
237    return rootBuilder;
238  }
239  
240  private final void addFile(final NavigableMap<String, Chart.Builder> chartBuilders, final String path, final InputStream stream) throws IOException {
241    Objects.requireNonNull(chartBuilders);
242    Objects.requireNonNull(path);
243    Objects.requireNonNull(stream);
244    
245    final Chart.Builder builder = getChartBuilder(chartBuilders, path);
246    if (builder == null) {
247      throw new IllegalStateException();
248    }
249    
250    final Object templateBuilder;
251    final boolean subchartFile;
252    String fileName = getTemplateFileName(path);
253    if (fileName == null) {
254      // Not a template file, not even in a subchart.
255      templateBuilder = null;      
256      fileName = getSubchartFileName(path);
257      if (fileName == null) {
258        // Not a subchart file or a template file so add it to the
259        // root builder.
260        subchartFile = false;
261        fileName = getOrdinaryFileName(path);
262      } else {
263        subchartFile = true;
264      }
265    } else {
266      subchartFile = false;
267      templateBuilder = this.createTemplateBuilder(builder, stream, fileName);
268    }
269    assert fileName != null;
270    if (templateBuilder == null) {
271      switch (fileName) {
272      case "Chart.yaml":
273        this.installMetadata(builder, stream);
274        break;
275      case "values.yaml":
276        this.installConfig(builder, stream);
277        break;
278      default:
279        if (subchartFile) {
280          if (fileName.endsWith(".prov")) {
281            // The intent in the Go code, despite its implementation,
282            // seems to be that a charts/foo.prov file should be
283            // treated as an ordinary file whose name is, well,
284            // charts/foo.prov, no matter how deep that directory
285            // hierarchy is, and despite that fact that the .prov file
286            // appears in a charts directory, which normally indicates
287            // the presence of a subchart.
288            // 
289            // So ordinarily we'd be in a subchart here.  Let's say we're:
290            //
291            //   wordpress/charts/argle/charts/foo/charts/bar/grob/foobish/.blatz.prov.
292            //
293            // We don't want the Chart.Builder associated with
294            // wordpress/charts/argle/charts/foo/charts/bar.  We want
295            // the Chart.Builder associated with
296            // wordpress/charts/argle/charts/foo.  And we want the
297            // filename added to that builder to be
298            // charts/bar/grob/foobish/.blatz.prov.  Let's take
299            // advantage of the sorted nature of the chartBuilders Map
300            // and look for our parent that way.
301            final Entry<String, Chart.Builder> parentChartBuilderEntry = chartBuilders.lowerEntry(path);
302            if (parentChartBuilderEntry == null) {
303              throw new IllegalStateException("chartBuilders.lowerEntry(path) == null; path: " + path);
304            }
305            final String parentChartPath = parentChartBuilderEntry.getKey();
306            final Chart.Builder parentChartBuilder = parentChartBuilderEntry.getValue();
307            if (parentChartBuilder == null) {
308              throw new IllegalStateException("chartBuilders.lowerEntry(path).getValue() == null; path: " + path);
309            }
310            final int prefixLength = ((parentChartPath == null ? "" : parentChartPath) + "/").length();
311            assert path.length() > prefixLength;
312            this.installAny(parentChartBuilder, stream, path.substring(prefixLength));
313          } else if (!(fileName.startsWith("_") || fileName.startsWith(".")) &&
314                     fileName.endsWith(".tgz") &&
315                     fileName.equals(basename(fileName))) {
316            assert fileName.indexOf('/') < 0;
317            // A subchart *file* (i.e. not a directory) that is not a
318            // .prov file, that is immediately beneath charts, that
319            // doesn't start with '.' or '_', and that ends with .tgz.
320            // Treat it as a tarball.
321            //
322            // So:  wordpress/charts/foo.tgz
323            // Not: wordpress/charts/.foo.tgz
324            // Not: wordpress/charts/_foo.tgz
325            // Not: wordpress/charts/foo
326            // Not: wordpress/charts/bar/foo.tgz
327            // Not: wordpress/charts/_bar/foo.tgz
328            Chart.Builder subchartBuilder = null;
329            try (final TarInputStream tarInputStream = new TarInputStream(new GZIPInputStream(new NonClosingInputStream(stream)))) {
330              subchartBuilder = new TapeArchiveChartLoader().load(builder, tarInputStream);
331            }
332            if (subchartBuilder == null) {
333              throw new IllegalStateException("load(builder, tarInputStream) == null; path: " + path);
334            }
335            // builder.addDependencies(subchart);
336          } else {
337            // Not a .prov file under charts, nor a .tgz file, just a
338            // regular subchart file.
339            this.installAny(builder, stream, fileName);
340          }
341        } else {
342          assert !subchartFile;
343          // Not a subchart file or a template
344          this.installAny(builder, stream, fileName);
345        }
346        break;
347      }
348    }
349  }
350  
351  static final String getOrdinaryFileName(final String path) {
352    String returnValue = null;
353    if (path != null) {
354      final Matcher fileMatcher = fileNamePattern.matcher(path);
355      assert fileMatcher != null;
356      if (fileMatcher.find()) {
357        returnValue = fileMatcher.group(1);
358      }
359    }
360    return returnValue;
361  }
362  
363  static final String getSubchartFileName(final String path) {
364    String returnValue = null;
365    if (path != null) {
366      final Matcher subchartMatcher = subchartFileNamePattern.matcher(path);
367      assert subchartMatcher != null;
368      if (subchartMatcher.find()) {
369        returnValue = subchartMatcher.group(1);
370      }
371    }
372    return returnValue;
373
374  }
375  
376  static final String getTemplateFileName(final String path) {
377    String returnValue = null;
378    if (path != null) {
379      final Matcher templateMatcher = templateFileNamePattern.matcher(path);
380      assert templateMatcher != null;
381      if (templateMatcher.find()) {
382        returnValue = templateMatcher.group(1);
383      }
384    }
385    return returnValue;
386  }
387
388  /**
389   * Given a semantic solidus-separated {@code chartPath} representing
390   * a file or logical directory within a chart, returns the proper
391   * {@link Chart.Builder} corresponding to that path.
392   *
393   * <p>This method never returns {@code null}.</p>
394   *
395   * <p>Any intermediate {@link Chart.Builder}s will also be created
396   * and properly parented.</p>
397   *
398   * @param chartBuilders a {@link Map} of {@link Chart.Builder}
399   * instances indexed by paths; must not be {@code null}; may be
400   * updated by this method
401   *
402   * @param chartPath a solidus-separated {@link String} representing
403   * a file or directory within a chart; must not be {@code null}
404   *
405   * @return a {@link Chart.Builder}; never {@code null}
406   *
407   * @exception NullPointerException if either {@code chartBuilders}
408   * or {@code chartPath} is {@code null}
409   */
410  private static final Chart.Builder getChartBuilder(final Map<String, Chart.Builder> chartBuilders, final String chartPath) {
411    Objects.requireNonNull(chartBuilders);
412    Objects.requireNonNull(chartPath);
413    Chart.Builder rootBuilder = chartBuilders.get(null);
414    if (rootBuilder == null) {
415      rootBuilder = Chart.newBuilder();
416      chartBuilders.put(null, rootBuilder);
417    }
418    assert rootBuilder != null;
419    Chart.Builder returnValue = rootBuilder;
420    final Collection<? extends String> chartPaths = toSubcharts(chartPath);
421    if (chartPaths != null && !chartPaths.isEmpty()) {
422      for (final String path : chartPaths) {
423        // By contract, shallowest path comes first, so
424        // foobar/charts/wordpress comes before, say,
425        // foobar/charts/wordpress/charts/mysql
426        Chart.Builder builder = chartBuilders.get(path);
427        if (builder == null) {
428          builder = createSubchartBuilder(returnValue, path);
429          assert builder != null;
430          chartBuilders.put(path, builder);
431        }
432        assert builder != null;
433        returnValue = builder;
434      }
435    }
436    assert returnValue != null;
437    return returnValue;
438  }
439
440  /**
441   * Given, e.g., {@code wordpress/charts/argle/charts/frob/foo.txt},
442   * yield {@code [ wordpress/charts/argle,
443   * wordpress/charts/argle/charts/frob ]}.
444   *
445   * <p>This method never returns {@code null}.</p>
446   *
447   * @param chartPath the "relative" solidus-separated path
448   * identifying some chart resource; must not be {@code null}
449   *
450   * @return a {@link NavigableSet} of chart paths in ascending
451   * subchart hierarchy order; never {@code null}
452   */
453  static final NavigableSet<String> toSubcharts(final String chartPath) {
454    Objects.requireNonNull(chartPath);
455    final NavigableSet<String> returnValue = new TreeSet<>(new ChartPathComparator());
456    final Matcher matcher = nonGreedySubchartsPattern.matcher(chartPath);
457    if (matcher != null) {
458      while (matcher.find()) {
459        returnValue.add(chartPath.substring(0, matcher.end()));
460      }
461    }
462    return returnValue;
463  }
464
465  private static final Chart.Builder createSubchartBuilder(final Chart.Builder parentBuilder, final String chartPath) {
466    Objects.requireNonNull(parentBuilder);
467    Chart.Builder returnValue = null;
468    final String chartName = getChartName(chartPath);
469    if (chartName != null) {
470      returnValue = parentBuilder.addDependenciesBuilder();
471      assert returnValue != null;
472      final Metadata.Builder builder = returnValue.getMetadataBuilder();
473      assert builder != null;
474      builder.setName(chartName);
475    }
476    return returnValue;
477  }
478  
479  private static final String getChartName(final String chartPath) {
480    String returnValue = null;
481    if (chartPath != null) {
482      final Matcher matcher = chartNamePattern.matcher(chartPath);
483      assert matcher != null;
484      if (matcher.find()) {
485        returnValue = matcher.group(1);
486      }
487    }
488    return returnValue;
489  }
490
491  private static final String basename(final String path) {
492    String returnValue = null;
493    if (path != null) {
494      final Matcher matcher = basenamePattern.matcher(path);
495      assert matcher != null;
496      if (matcher.find()) {
497        returnValue = matcher.group(1);
498      }
499    }
500    return returnValue;
501  }
502  
503
504  /*
505   * Utility methods.
506   */
507  
508
509  /**
510   * Installs a {@link Config} object, represented by the supplied
511   * {@link InputStream}, into the supplied {@link
512   * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder}.
513   *
514   * @param chartBuilder the {@link
515   * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder} to
516   * affect; must not be {@code null}
517   *
518   * @param stream an {@link InputStream} representing <a
519   * href="https://docs.helm.sh/developing_charts/#values-files">valid
520   * values file contents</a> as defined by <a
521   * href="https://docs.helm.sh/developing_charts/#values-files">the
522   * chart specification</a>; must not be {@code null}
523   *
524   * @exception NullPointerException if {@code chartBuilder} or {@code
525   * stream} is {@code null}
526   *
527   * @exception IOException if there was a problem reading from the
528   * supplied {@link InputStream}
529   *
530   * @see hapi.chart.ChartOuterClass.Chart.Builder#getValuesBuilder()
531   *
532   * @see hapi.chart.ConfigOuterClass.Config.Builder#setRawBytes(ByteString)
533   */
534  protected void installConfig(final Chart.Builder chartBuilder, final InputStream stream) throws IOException {
535    Objects.requireNonNull(chartBuilder);
536    Objects.requireNonNull(stream);
537    Config returnValue = null;
538    final Config.Builder builder = chartBuilder.getValuesBuilder();
539    assert builder != null;
540    final ByteString rawBytes = ByteString.readFrom(stream);
541    assert rawBytes != null;
542    builder.setRawBytes(rawBytes);
543  }
544
545  /**
546   * Installs a {@link Metadata} object, represented by the supplied
547   * {@link InputStream}, into the supplied {@link
548   * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder}.
549   *
550   * @param chartBuilder the {@link
551   * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder} to
552   * affect; must not be {@code null}
553   *
554   * @param stream an {@link InputStream} representing <a
555   * href="https://docs.helm.sh/developing_charts/#the-chart-yaml-file">valid
556   * {@code Chart.yaml} contents</a> as defined by <a
557   * href="https://docs.helm.sh/developing_charts/#the-chart-yaml-file">the
558   * chart specification</a>; must not be {@code null}
559   *
560   * @exception NullPointerException if {@code chartBuilder} or {@code
561   * stream} is {@code null}
562   *
563   * @exception IOException if there was a problem reading from the
564   * supplied {@link InputStream}
565   *
566   * @see hapi.chart.ChartOuterClass.Chart.Builder#getMetadataBuilder()
567   *
568   * @see hapi.chart.MetadataOuterClass.Metadata.Builder
569   */
570  protected void installMetadata(final Chart.Builder chartBuilder, final InputStream stream) throws IOException {
571    Objects.requireNonNull(chartBuilder);
572    Objects.requireNonNull(stream);
573    Metadata returnValue = null;
574    final Map<?, ?> map = new Yaml().loadAs(stream, Map.class);
575    assert map != null;
576    final Metadata.Builder metadataBuilder = chartBuilder.getMetadataBuilder();
577    assert metadataBuilder != null;
578    Metadatas.populateMetadataBuilder(metadataBuilder, map);
579  }
580
581  /**
582   * {@linkplain
583   * hapi.chart.ChartOuterClass.Chart.Builder#addTemplatesBuilder()
584   * Creates a new} {@link
585   * hapi.chart.TemplateOuterClass.Template.Builder} {@linkplain
586   * hapi.chart.TemplateOuterClass.Template.Builder#setData(ByteString)
587   * from the contents of the supplied <code>InputStream</code>},
588   * {@linkplain
589   * hapi.chart.TemplateOuterClass.Template.Builder#setName(String)
590   * with the supplied <code>name</code>}, and returns it.
591   *
592   * <p>This method never returns {@code null}.</p>
593   *
594   * @param chartBuilder a {@link
595   * hapi.chart.ChartOuterClass.Chart.Builder} whose {@link
596   * hapi.chart.ChartOuterClass.Chart.Builder#addTemplatesBuilder()}
597   * method will be called to create the new {@link
598   * hapi.chart.TemplateOuterClass.Template.Builder} instance; must
599   * not be {@code null}
600   *
601   * @param stream an {@link InputStream} containing <a
602   * href="https://docs.helm.sh/developing_charts/#template-files">valid
603   * template contents</a> as defined by the <a
604   * href="https://docs.helm.sh/developing_charts/#template-files">chart
605   * specification</a>; must not be {@code null}
606   *
607   * @param name the name for the new {@link Template} that will
608   * ultimately reside within the chart; must not be {@code null}
609   *
610   * @return a new {@link
611   * hapi.chart.TemplateOuterClass.Template.Builder}; never {@code
612   * null}
613   *
614   * @exception NullPointerException if {@code chartBuilder}, {@code
615   * stream} or {@code name} is {@code null}
616   *
617   * @exception IOException if there was a problem reading from the
618   * supplied {@link InputStream}
619   *
620   * @see hapi.chart.TemplateOuterClass.Template.Builder
621   */
622  protected Template.Builder createTemplateBuilder(final Chart.Builder chartBuilder, final InputStream stream, final String name) throws IOException {
623    Objects.requireNonNull(chartBuilder);
624    Objects.requireNonNull(stream);
625    Objects.requireNonNull(name);
626    final Template.Builder builder = chartBuilder.addTemplatesBuilder();
627    assert builder != null;
628    builder.setName(name);
629    final ByteString data = ByteString.readFrom(stream);
630    assert data != null;
631    assert data.isValidUtf8();
632    builder.setData(data);
633    return builder;
634  }
635
636  /**
637   * Installs an {@link Any} object, representing an arbitrary chart
638   * file with the supplied {@code name} and represented by the
639   * supplied {@link InputStream}, into the supplied {@link
640   * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder}.
641   *
642   * @param chartBuilder the {@link
643   * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder} to
644   * affect; must not be {@code null}
645   *
646   * @param stream an {@link InputStream} representing <a
647   * href="https://docs.helm.sh/developing_charts/">valid chart file
648   * contents</a> as defined by <a
649   * href="https://docs.helm.sh/developing_charts/">the chart
650   * specification</a>; must not be {@code null}
651   *
652   * @param name the name of the file within the chart; must not be
653   * {@code null}
654   *
655   * @exception NullPointerException if {@code chartBuilder} or {@code
656   * stream} or {@code name} is {@code null}
657   *
658   * @exception IOException if there was a problem reading from the
659   * supplied {@link InputStream}
660   *
661   * @see hapi.chart.ChartOuterClass.Chart.Builder#addFilesBuilder()
662   */
663  protected void installAny(final Chart.Builder chartBuilder, final InputStream stream, final String name) throws IOException {
664    Objects.requireNonNull(chartBuilder);
665    Objects.requireNonNull(stream);
666    Objects.requireNonNull(name);
667    Any returnValue = null;
668    final Any.Builder builder = chartBuilder.addFilesBuilder();
669    assert builder != null;
670    builder.setTypeUrl(name);
671    final ByteString fileContents = ByteString.readFrom(stream);
672    assert fileContents != null;
673    assert fileContents.isValidUtf8();
674    builder.setValue(fileContents);
675  }
676
677
678  /*
679   * Inner and nested classes.
680   */
681
682
683  /**
684   * An {@link Iterable} implementation that {@linkplain #iterator()
685   * returns an empty <code>Iterator</code>}.
686   *
687   * @author <a href="https://about.me/lairdnelson"
688   * target="_parent">Laird Nelson</a>
689   */
690  static final class EmptyIterable implements Iterable<Entry<String, InputStream>> {
691
692
693    /*
694     * Constructors.
695     */
696
697
698    /**
699     * Creates a new {@link EmptyIterable}.
700     */
701    EmptyIterable() {
702      super();
703    }
704
705
706    /*
707     * Instance methods.
708     */
709    
710
711    /**
712     * Returns the return value of the {@link
713     * Collections#emptyIterator()} method when invoked.
714     *
715     * <p>This method never returns {@code null}.</p>
716     *
717     * @return an empty {@link Iterator}; never {@code null}
718     */
719    @Override
720    public final Iterator<Entry<String, InputStream>> iterator() {
721      return Collections.emptyIterator();
722    }
723    
724  }
725
726
727  
728  private static final class ChartPathComparator implements Comparator<String> {
729
730    private ChartPathComparator() {
731      super();
732    }
733
734    @Override
735    public final int compare(final String chartPath1, final String chartPath2) {
736      if (chartPath1 == null) {
737        if (chartPath2 == null) {
738          return 0;
739        } else {
740          return -1; // nulls go to the left
741        }
742      } else if (chartPath1.equals(chartPath2)) {
743        return 0;
744      } else if (chartPath2 == null) {
745        return 1;
746      } else {
747        final int chartPath1Length = chartPath1.length();
748        final int chartPath2Length = chartPath2.length();
749        if (chartPath1Length == chartPath2Length) {
750          return chartPath1.compareTo(chartPath2);
751        } else if (chartPath1Length > chartPath2Length) {
752          return 1;
753        } else {
754          return -1;
755        }
756      }
757    }
758    
759  }
760  
761}