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