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