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