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