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}