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