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