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