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 * @param source the source to convert; must not be {@code null} 149 * 150 * @return an {@link Iterable} of suitable {@link Entry} instances; 151 * never {@code null} 152 * 153 * @exception NullPointerException if {@code source} is {@code null} 154 * 155 * @exception IOException if an error occurs while converting 156 */ 157 protected abstract Iterable<? extends Entry<? extends String, ? extends InputStream>> toNamedInputStreamEntries(final T source) throws IOException; 158 159 /** 160 * Creates a new {@link Chart} from the supplied {@code source} in 161 * some manner and returns it. 162 * 163 * <p>This method never returns {@code null}. 164 * 165 * <p>This method calls the {@link 166 * #load(hapi.chart.ChartOuterClass.Chart.Builder, Iterable)} method 167 * with the return value of the {@link 168 * #toNamedInputStreamEntries(Object)} method.</p> 169 * 170 * @param source the source object from which to load a new {@link 171 * Chart}; must not be {@code null} 172 * 173 * @return a new {@link Chart}; never {@code null} 174 * 175 * @exception NullPointerException if {@code source} is {@code null} 176 * 177 * @exception IllegalStateException if the {@link 178 * #load(hapi.chart.ChartOuterClass.Chart.Builder, Iterable)} method 179 * returns {@code null} 180 * 181 * @exception IOException if a problem is encountered while creating 182 * the {@link Chart} to return 183 * 184 * @see #toNamedInputStreamEntries(Object) 185 * 186 * @see #load(hapi.chart.ChartOuterClass.Chart.Builder, Iterable) 187 */ 188 @Override 189 public Chart.Builder load(final Chart.Builder parent, final T source) throws IOException { 190 Objects.requireNonNull(source); 191 final Chart.Builder returnValue = this.load(parent, toNamedInputStreamEntries(source)); 192 if (returnValue == null) { 193 throw new IllegalStateException("load(toNamedInputStreamEntries(source)) == null; source: " + source); 194 } 195 return returnValue; 196 } 197 198 /** 199 * Creates a new {@link Chart} from the supplied notional set of 200 * named {@link InputStream}s and returns it. 201 * 202 * <p>This method never returns {@code null}. 203 * 204 * <p>This method is called by the {@link #load(Object)} method.</p> 205 * 206 * @param entrySet the {@link Iterable} of {@link Entry} instances 207 * normally returned by the {@link 208 * #toNamedInputStreamEntries(Object)} method; must not be {@code 209 * null} 210 * 211 * @return a new {@link Chart}; never {@code null} 212 * 213 * @exception NullPointerException if {@code entrySet} is {@code 214 * null} 215 * 216 * @exception IOException if a problem is encountered while creating 217 * the {@link Chart} to return 218 * 219 * @see #toNamedInputStreamEntries(Object) 220 * 221 * @see #load(Object) 222 */ 223 public Chart.Builder load(final Chart.Builder parent, final Iterable<? extends Entry<? extends String, ? extends InputStream>> entrySet) throws IOException { 224 Objects.requireNonNull(entrySet); 225 final Chart.Builder rootBuilder; 226 if (parent == null) { 227 rootBuilder = Chart.newBuilder(); 228 } else { 229 rootBuilder = parent; 230 } 231 assert rootBuilder != null; 232 final NavigableMap<String, Chart.Builder> chartBuilders = new TreeMap<>(new ChartPathComparator()); 233 // XXX TODO FIXME: do we really want to say the root is null? 234 // Or should it always be a path named after the chart? 235 chartBuilders.put(null, rootBuilder); 236 for (final Entry<? extends String, ? extends InputStream> entry : entrySet) { 237 if (entry != null) { 238 this.addFile(chartBuilders, entry.getKey(), entry.getValue()); 239 } 240 } 241 return rootBuilder; 242 } 243 244 private final void addFile(final NavigableMap<String, Chart.Builder> chartBuilders, final String path, final InputStream stream) throws IOException { 245 Objects.requireNonNull(chartBuilders); 246 Objects.requireNonNull(path); 247 Objects.requireNonNull(stream); 248 249 final Chart.Builder builder = getChartBuilder(chartBuilders, path); 250 if (builder == null) { 251 throw new IllegalStateException(); 252 } 253 254 final Object templateBuilder; 255 final boolean subchartFile; 256 String fileName = getTemplateFileName(path); 257 if (fileName == null) { 258 // Not a template file, not even in a subchart. 259 templateBuilder = null; 260 fileName = getSubchartFileName(path); 261 if (fileName == null) { 262 // Not a subchart file or a template file so add it to the 263 // root builder. 264 subchartFile = false; 265 fileName = getOrdinaryFileName(path); 266 } else { 267 subchartFile = true; 268 } 269 } else { 270 subchartFile = false; 271 templateBuilder = this.createTemplateBuilder(builder, stream, fileName); 272 } 273 assert fileName != null; 274 if (templateBuilder == null) { 275 switch (fileName) { 276 case "Chart.yaml": 277 this.installMetadata(builder, stream); 278 break; 279 case "values.yaml": 280 this.installConfig(builder, stream); 281 break; 282 default: 283 if (subchartFile) { 284 if (fileName.endsWith(".prov")) { 285 // The intent in the Go code, despite its implementation, 286 // seems to be that a charts/foo.prov file should be 287 // treated as an ordinary file whose name is, well, 288 // charts/foo.prov, no matter how deep that directory 289 // hierarchy is, and despite that fact that the .prov file 290 // appears in a charts directory, which normally indicates 291 // the presence of a subchart. 292 // 293 // So ordinarily we'd be in a subchart here. Let's say we're: 294 // 295 // wordpress/charts/argle/charts/foo/charts/bar/grob/foobish/.blatz.prov. 296 // 297 // We don't want the Chart.Builder associated with 298 // wordpress/charts/argle/charts/foo/charts/bar. We want 299 // the Chart.Builder associated with 300 // wordpress/charts/argle/charts/foo. And we want the 301 // filename added to that builder to be 302 // charts/bar/grob/foobish/.blatz.prov. Let's take 303 // advantage of the sorted nature of the chartBuilders Map 304 // and look for our parent that way. 305 final Entry<String, Chart.Builder> parentChartBuilderEntry = chartBuilders.lowerEntry(path); 306 if (parentChartBuilderEntry == null) { 307 throw new IllegalStateException("chartBuilders.lowerEntry(path) == null; path: " + path); 308 } 309 final String parentChartPath = parentChartBuilderEntry.getKey(); 310 final Chart.Builder parentChartBuilder = parentChartBuilderEntry.getValue(); 311 if (parentChartBuilder == null) { 312 throw new IllegalStateException("chartBuilders.lowerEntry(path).getValue() == null; path: " + path); 313 } 314 final int prefixLength = ((parentChartPath == null ? "" : parentChartPath) + "/").length(); 315 assert path.length() > prefixLength; 316 this.installAny(parentChartBuilder, stream, path.substring(prefixLength)); 317 } else if (!(fileName.startsWith("_") || fileName.startsWith(".")) && 318 fileName.endsWith(".tgz") && 319 fileName.equals(basename(fileName))) { 320 assert fileName.indexOf('/') < 0; 321 // A subchart *file* (i.e. not a directory) that is not a 322 // .prov file, that is immediately beneath charts, that 323 // doesn't start with '.' or '_', and that ends with .tgz. 324 // Treat it as a tarball. 325 // 326 // So: wordpress/charts/foo.tgz 327 // Not: wordpress/charts/.foo.tgz 328 // Not: wordpress/charts/_foo.tgz 329 // Not: wordpress/charts/foo 330 // Not: wordpress/charts/bar/foo.tgz 331 // Not: wordpress/charts/_bar/foo.tgz 332 Chart.Builder subchartBuilder = null; 333 try (final TarInputStream tarInputStream = new TarInputStream(new GZIPInputStream(new NonClosingInputStream(stream)))) { 334 subchartBuilder = new TapeArchiveChartLoader().load(builder, tarInputStream); 335 } 336 if (subchartBuilder == null) { 337 throw new IllegalStateException("load(builder, tarInputStream) == null; path: " + path); 338 } 339 // builder.addDependencies(subchart); 340 } else { 341 // Not a .prov file under charts, nor a .tgz file, just a 342 // regular subchart file. 343 this.installAny(builder, stream, fileName); 344 } 345 } else { 346 assert !subchartFile; 347 // Not a subchart file or a template 348 this.installAny(builder, stream, fileName); 349 } 350 break; 351 } 352 } 353 } 354 355 static final String getOrdinaryFileName(final String path) { 356 String returnValue = null; 357 if (path != null) { 358 final Matcher fileMatcher = fileNamePattern.matcher(path); 359 assert fileMatcher != null; 360 if (fileMatcher.find()) { 361 returnValue = fileMatcher.group(1); 362 } 363 } 364 return returnValue; 365 } 366 367 static final String getSubchartFileName(final String path) { 368 String returnValue = null; 369 if (path != null) { 370 final Matcher subchartMatcher = subchartFileNamePattern.matcher(path); 371 assert subchartMatcher != null; 372 if (subchartMatcher.find()) { 373 // in foo/charts/bork/blatz.txt: 374 // group 1 is bork/blatz.txt 375 // group 2 is blatz.txt 376 // in foo/charts/blatz.tgz: 377 // group 1 is blatz.tgz 378 // group 2 is (empty string) 379 final String group2 = subchartMatcher.group(2); 380 assert group2 != null; 381 if (group2.isEmpty()) { 382 returnValue = subchartMatcher.group(1); 383 assert returnValue != null; 384 } else { 385 returnValue = group2; 386 } 387 } 388 } 389 return returnValue; 390 391 } 392 393 static final String getTemplateFileName(final String path) { 394 String returnValue = null; 395 if (path != null) { 396 final Matcher templateMatcher = templateFileNamePattern.matcher(path); 397 assert templateMatcher != null; 398 if (templateMatcher.find()) { 399 returnValue = templateMatcher.group(1); 400 } 401 } 402 return returnValue; 403 } 404 405 /** 406 * Given a semantic solidus-separated {@code chartPath} representing 407 * a file or logical directory within a chart, returns the proper 408 * {@link Chart.Builder} corresponding to that path. 409 * 410 * <p>This method never returns {@code null}.</p> 411 * 412 * <p>Any intermediate {@link Chart.Builder}s will also be created 413 * and properly parented.</p> 414 * 415 * @param chartBuilders a {@link Map} of {@link Chart.Builder} 416 * instances indexed by paths; must not be {@code null}; may be 417 * updated by this method 418 * 419 * @param chartPath a solidus-separated {@link String} representing 420 * a file or directory within a chart; must not be {@code null} 421 * 422 * @return a {@link Chart.Builder}; never {@code null} 423 * 424 * @exception NullPointerException if either {@code chartBuilders} 425 * or {@code chartPath} is {@code null} 426 */ 427 private static final Chart.Builder getChartBuilder(final Map<String, Chart.Builder> chartBuilders, final String chartPath) { 428 Objects.requireNonNull(chartBuilders); 429 Objects.requireNonNull(chartPath); 430 Chart.Builder rootBuilder = chartBuilders.get(null); 431 if (rootBuilder == null) { 432 rootBuilder = Chart.newBuilder(); 433 chartBuilders.put(null, rootBuilder); 434 } 435 assert rootBuilder != null; 436 Chart.Builder returnValue = rootBuilder; 437 final Collection<? extends String> chartPaths = toSubcharts(chartPath); 438 if (chartPaths != null && !chartPaths.isEmpty()) { 439 for (final String path : chartPaths) { 440 // By contract, shallowest path comes first, so 441 // foobar/charts/wordpress comes before, say, 442 // foobar/charts/wordpress/charts/mysql 443 Chart.Builder builder = chartBuilders.get(path); 444 if (builder == null) { 445 builder = createSubchartBuilder(returnValue, path); 446 assert builder != null; 447 chartBuilders.put(path, builder); 448 } 449 assert builder != null; 450 returnValue = builder; 451 } 452 } 453 assert returnValue != null; 454 return returnValue; 455 } 456 457 /** 458 * Given, e.g., {@code wordpress/charts/argle/charts/frob/foo.txt}, 459 * yield {@code [ wordpress/charts/argle, 460 * wordpress/charts/argle/charts/frob ]}. 461 * 462 * <p>This method never returns {@code null}.</p> 463 * 464 * @param chartPath the "relative" solidus-separated path 465 * identifying some chart resource; must not be {@code null} 466 * 467 * @return a {@link NavigableSet} of chart paths in ascending 468 * subchart hierarchy order; never {@code null} 469 */ 470 static final NavigableSet<String> toSubcharts(final String chartPath) { 471 Objects.requireNonNull(chartPath); 472 final NavigableSet<String> returnValue = new TreeSet<>(new ChartPathComparator()); 473 final Matcher matcher = nonGreedySubchartsPattern.matcher(chartPath); 474 if (matcher != null) { 475 while (matcher.find()) { 476 returnValue.add(chartPath.substring(0, matcher.end())); 477 } 478 } 479 return returnValue; 480 } 481 482 private static final Chart.Builder createSubchartBuilder(final Chart.Builder parentBuilder, final String chartPath) { 483 Objects.requireNonNull(parentBuilder); 484 Chart.Builder returnValue = null; 485 final String chartName = getChartName(chartPath); 486 if (chartName != null) { 487 returnValue = parentBuilder.addDependenciesBuilder(); 488 assert returnValue != null; 489 final Metadata.Builder builder = returnValue.getMetadataBuilder(); 490 assert builder != null; 491 builder.setName(chartName); 492 } 493 return returnValue; 494 } 495 496 private static final String getChartName(final String chartPath) { 497 String returnValue = null; 498 if (chartPath != null) { 499 final Matcher matcher = chartNamePattern.matcher(chartPath); 500 assert matcher != null; 501 if (matcher.find()) { 502 returnValue = matcher.group(1); 503 } 504 } 505 return returnValue; 506 } 507 508 private static final String basename(final String path) { 509 String returnValue = null; 510 if (path != null) { 511 final Matcher matcher = basenamePattern.matcher(path); 512 assert matcher != null; 513 if (matcher.find()) { 514 returnValue = matcher.group(1); 515 } 516 } 517 return returnValue; 518 } 519 520 521 /* 522 * Utility methods. 523 */ 524 525 526 /** 527 * Installs a {@link Config} object, represented by the supplied 528 * {@link InputStream}, into the supplied {@link 529 * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder}. 530 * 531 * @param chartBuilder the {@link 532 * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder} to 533 * affect; must not be {@code null} 534 * 535 * @param stream an {@link InputStream} representing <a 536 * href="https://docs.helm.sh/developing_charts/#values-files">valid 537 * values file contents</a> as defined by <a 538 * href="https://docs.helm.sh/developing_charts/#values-files">the 539 * chart specification</a>; must not be {@code null} 540 * 541 * @exception NullPointerException if {@code chartBuilder} or {@code 542 * stream} is {@code null} 543 * 544 * @exception IOException if there was a problem reading from the 545 * supplied {@link InputStream} 546 * 547 * @see hapi.chart.ChartOuterClass.Chart.Builder#getValuesBuilder() 548 * 549 * @see hapi.chart.ConfigOuterClass.Config.Builder#setRawBytes(ByteString) 550 */ 551 protected void installConfig(final Chart.Builder chartBuilder, final InputStream stream) throws IOException { 552 Objects.requireNonNull(chartBuilder); 553 Objects.requireNonNull(stream); 554 Config returnValue = null; 555 final Config.Builder builder = chartBuilder.getValuesBuilder(); 556 assert builder != null; 557 final ByteString rawBytes = ByteString.readFrom(stream); 558 assert rawBytes != null; 559 builder.setRawBytes(rawBytes); 560 } 561 562 /** 563 * Installs a {@link Metadata} object, represented by the supplied 564 * {@link InputStream}, into the supplied {@link 565 * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder}. 566 * 567 * @param chartBuilder the {@link 568 * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder} to 569 * affect; must not be {@code null} 570 * 571 * @param stream an {@link InputStream} representing <a 572 * href="https://docs.helm.sh/developing_charts/#the-chart-yaml-file">valid 573 * {@code Chart.yaml} contents</a> as defined by <a 574 * href="https://docs.helm.sh/developing_charts/#the-chart-yaml-file">the 575 * chart specification</a>; must not be {@code null} 576 * 577 * @exception NullPointerException if {@code chartBuilder} or {@code 578 * stream} is {@code null} 579 * 580 * @exception IOException if there was a problem reading from the 581 * supplied {@link InputStream} 582 * 583 * @see hapi.chart.ChartOuterClass.Chart.Builder#getMetadataBuilder() 584 * 585 * @see hapi.chart.MetadataOuterClass.Metadata.Builder 586 */ 587 protected void installMetadata(final Chart.Builder chartBuilder, final InputStream stream) throws IOException { 588 Objects.requireNonNull(chartBuilder); 589 Objects.requireNonNull(stream); 590 Metadata returnValue = null; 591 final Map<?, ?> map = new Yaml().loadAs(stream, Map.class); 592 assert map != null; 593 final Metadata.Builder metadataBuilder = chartBuilder.getMetadataBuilder(); 594 assert metadataBuilder != null; 595 Metadatas.populateMetadataBuilder(metadataBuilder, map); 596 } 597 598 /** 599 * {@linkplain 600 * hapi.chart.ChartOuterClass.Chart.Builder#addTemplatesBuilder() 601 * Creates a new} {@link 602 * hapi.chart.TemplateOuterClass.Template.Builder} {@linkplain 603 * hapi.chart.TemplateOuterClass.Template.Builder#setData(ByteString) 604 * from the contents of the supplied <code>InputStream</code>}, 605 * {@linkplain 606 * hapi.chart.TemplateOuterClass.Template.Builder#setName(String) 607 * with the supplied <code>name</code>}, and returns it. 608 * 609 * <p>This method never returns {@code null}.</p> 610 * 611 * @param chartBuilder a {@link 612 * hapi.chart.ChartOuterClass.Chart.Builder} whose {@link 613 * hapi.chart.ChartOuterClass.Chart.Builder#addTemplatesBuilder()} 614 * method will be called to create the new {@link 615 * hapi.chart.TemplateOuterClass.Template.Builder} instance; must 616 * not be {@code null} 617 * 618 * @param stream an {@link InputStream} containing <a 619 * href="https://docs.helm.sh/developing_charts/#template-files">valid 620 * template contents</a> as defined by the <a 621 * href="https://docs.helm.sh/developing_charts/#template-files">chart 622 * specification</a>; must not be {@code null} 623 * 624 * @param name the name for the new {@link Template} that will 625 * ultimately reside within the chart; must not be {@code null} 626 * 627 * @return a new {@link 628 * hapi.chart.TemplateOuterClass.Template.Builder}; never {@code 629 * null} 630 * 631 * @exception NullPointerException if {@code chartBuilder}, {@code 632 * stream} or {@code name} is {@code null} 633 * 634 * @exception IOException if there was a problem reading from the 635 * supplied {@link InputStream} 636 * 637 * @see hapi.chart.TemplateOuterClass.Template.Builder 638 */ 639 protected Template.Builder createTemplateBuilder(final Chart.Builder chartBuilder, final InputStream stream, final String name) throws IOException { 640 Objects.requireNonNull(chartBuilder); 641 Objects.requireNonNull(stream); 642 Objects.requireNonNull(name); 643 final Template.Builder builder = chartBuilder.addTemplatesBuilder(); 644 assert builder != null; 645 builder.setName(name); 646 final ByteString data = ByteString.readFrom(stream); 647 assert data != null; 648 assert data.isValidUtf8(); 649 builder.setData(data); 650 return builder; 651 } 652 653 /** 654 * Installs an {@link Any} object, representing an arbitrary chart 655 * file with the supplied {@code name} and represented by the 656 * supplied {@link InputStream}, into the supplied {@link 657 * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder}. 658 * 659 * @param chartBuilder the {@link 660 * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder} to 661 * affect; must not be {@code null} 662 * 663 * @param stream an {@link InputStream} representing <a 664 * href="https://docs.helm.sh/developing_charts/">valid chart file 665 * contents</a> as defined by <a 666 * href="https://docs.helm.sh/developing_charts/">the chart 667 * specification</a>; must not be {@code null} 668 * 669 * @param name the name of the file within the chart; must not be 670 * {@code null} 671 * 672 * @exception NullPointerException if {@code chartBuilder} or {@code 673 * stream} or {@code name} is {@code null} 674 * 675 * @exception IOException if there was a problem reading from the 676 * supplied {@link InputStream} 677 * 678 * @see hapi.chart.ChartOuterClass.Chart.Builder#addFilesBuilder() 679 */ 680 protected void installAny(final Chart.Builder chartBuilder, final InputStream stream, final String name) throws IOException { 681 Objects.requireNonNull(chartBuilder); 682 Objects.requireNonNull(stream); 683 Objects.requireNonNull(name); 684 Any returnValue = null; 685 final Any.Builder builder = chartBuilder.addFilesBuilder(); 686 assert builder != null; 687 builder.setTypeUrl(name); 688 final ByteString fileContents = ByteString.readFrom(stream); 689 assert fileContents != null; 690 assert fileContents.isValidUtf8(); 691 builder.setValue(fileContents); 692 } 693 694 695 /* 696 * Inner and nested classes. 697 */ 698 699 700 /** 701 * An {@link Iterable} implementation that {@linkplain #iterator() 702 * returns an empty <code>Iterator</code>}. 703 * 704 * @author <a href="https://about.me/lairdnelson" 705 * target="_parent">Laird Nelson</a> 706 */ 707 static final class EmptyIterable implements Iterable<Entry<String, InputStream>> { 708 709 710 /* 711 * Constructors. 712 */ 713 714 715 /** 716 * Creates a new {@link EmptyIterable}. 717 */ 718 EmptyIterable() { 719 super(); 720 } 721 722 723 /* 724 * Instance methods. 725 */ 726 727 728 /** 729 * Returns the return value of the {@link 730 * Collections#emptyIterator()} method when invoked. 731 * 732 * <p>This method never returns {@code null}.</p> 733 * 734 * @return an empty {@link Iterator}; never {@code null} 735 */ 736 @Override 737 public final Iterator<Entry<String, InputStream>> iterator() { 738 return Collections.emptyIterator(); 739 } 740 741 } 742 743 744 745 private static final class ChartPathComparator implements Comparator<String> { 746 747 private ChartPathComparator() { 748 super(); 749 } 750 751 @Override 752 public final int compare(final String chartPath1, final String chartPath2) { 753 if (chartPath1 == null) { 754 if (chartPath2 == null) { 755 return 0; 756 } else { 757 return -1; // nulls go to the left 758 } 759 } else if (chartPath1.equals(chartPath2)) { 760 return 0; 761 } else if (chartPath2 == null) { 762 return 1; 763 } else { 764 final int chartPath1Length = chartPath1.length(); 765 final int chartPath2Length = chartPath2.length(); 766 if (chartPath1Length == chartPath2Length) { 767 return chartPath1.compareTo(chartPath2); 768 } else if (chartPath1Length > chartPath2Length) { 769 return 1; 770 } else { 771 return -1; 772 } 773 } 774 } 775 776 } 777 778}