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