001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017-2018 microBean. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.helm.chart; 018 019import java.beans.IntrospectionException; 020import java.beans.PropertyDescriptor; 021 022import java.io.Closeable; 023import java.io.IOException; 024 025import java.lang.reflect.Array; 026 027import java.util.Collection; 028import java.util.HashMap; 029import java.util.Map; 030import java.util.Objects; 031import java.util.TreeSet; 032import java.util.Set; 033 034import com.google.protobuf.AnyOrBuilder; 035 036import hapi.chart.ChartOuterClass.ChartOrBuilder; 037import hapi.chart.ConfigOuterClass.ConfigOrBuilder; 038import hapi.chart.MetadataOuterClass.MaintainerOrBuilder; 039import hapi.chart.MetadataOuterClass.MetadataOrBuilder; 040import hapi.chart.TemplateOuterClass.TemplateOrBuilder; 041 042import org.yaml.snakeyaml.DumperOptions; 043import org.yaml.snakeyaml.Yaml; 044 045import org.yaml.snakeyaml.constructor.SafeConstructor; 046 047import org.yaml.snakeyaml.introspector.BeanAccess; 048import org.yaml.snakeyaml.introspector.MethodProperty; 049import org.yaml.snakeyaml.introspector.Property; 050import org.yaml.snakeyaml.introspector.PropertyUtils; 051 052import org.yaml.snakeyaml.nodes.MappingNode; 053import org.yaml.snakeyaml.nodes.NodeTuple; 054import org.yaml.snakeyaml.nodes.Tag; 055 056import org.yaml.snakeyaml.representer.Representer; 057 058/** 059 * An object capable of writing or serializing or otherwise 060 * representing a {@link ChartOrBuilder}. 061 * 062 * @author <a href="https://about.me/lairdnelson" 063 * target="_parent">Laird Nelson</a> 064 * 065 * @see #write(ChartOuterClass.ChartOrBuilder) 066 */ 067public abstract class AbstractChartWriter implements Closeable { 068 069 070 /* 071 * Constructors. 072 */ 073 074 075 /** 076 * Creates a new {@link AbstractChartWriter}. 077 */ 078 protected AbstractChartWriter() { 079 super(); 080 } 081 082 083 /* 084 * Instance methods. 085 */ 086 087 088 /** 089 * Writes or serializes or otherwise represents the supplied {@link 090 * ChartOrBuilder}. 091 * 092 * @param chartBuilder the {@link ChartOrBuilder} to write; must not 093 * be {@code null} 094 * 095 * @exception IOException if a write error occurs 096 * 097 * @exception NullPointerException if {@code chartBuilder} is {@code 098 * null} 099 * 100 * @exception IllegalArgumentException if the {@link 101 * ChartOrBuilder#getMetadata()} method returns {@code null}, or if 102 * the {@link MetadataOrBuilder#getName()} method returns {@code 103 * null}, or if the {@link MetadataOrBuilder#getVersion()} method 104 * returns {@code null} 105 * 106 * @exception IllegalStateException if a subclass has overridden the 107 * {@link #createYaml()} method to return {@code null} and calls it 108 * 109 * @see #write(Context, ChartOuterClass.ChartOrBuilder, 110 * ChartOuterClass.ChartOrBuilder) 111 */ 112 public final void write(final ChartOrBuilder chartBuilder) throws IOException { 113 this.write(null, null, Objects.requireNonNull(chartBuilder)); 114 } 115 116 /** 117 * Writes or serializes or otherwise represents the supplied {@code 118 * chartBuilder} as a subchart of the supplied {@code parent} (which 119 * may be, and often is, {@code null}). 120 * 121 * @param context the {@link Context} representing the write 122 * operation; may be {@code null} 123 * 124 * @param parent the {@link ChartOrBuilder} functioning as the 125 * parent chart; may be, and often is, {@code null}; must not be 126 * identical to the {@code chartBuilder} parameter value 127 * 128 * @param chartBuilder the {@link ChartOrBuilder} to actually write; 129 * must not be {@code null}; must not be identical to the {@code 130 * parent} parameter value 131 * 132 * @exception IOException if a write error occurs 133 * 134 * @exception NullPointerException if {@code chartBuilder} is {@code null} 135 * 136 * @exception IllegalArgumentException if {@code parent} is 137 * identical to {@code chartBuilder}, or if the {@link 138 * ChartOrBuilder#getMetadata()} method returns {@code null}, or if 139 * the {@link MetadataOrBuilder#getName()} method returns {@code 140 * null}, or if the {@link MetadataOrBuilder#getVersion()} method 141 * returns {@code null} 142 * 143 * @exception IllegalStateException if a subclass has overridden the 144 * {@link #createYaml()} method to return {@code null} and calls it 145 * 146 * @see #beginWrite(Context, ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder) 147 * 148 * @see #writeMetadata(Context, MetadataOuterClass.MetadataOrBuilder) 149 * 150 * @see #writeConfig(Context, ConfigOuterClass.ConfigOrBuilder) 151 * 152 * @see #writeTemplate(Context, TemplateOuterClass.TemplateOrBuilder) 153 * 154 * @see #writeFile(Context, AnyOrBuilder) 155 * 156 * @see #writeSubchart(Context, ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder) 157 * 158 * @see #endWrite(Context, ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder) 159 */ 160 protected void write(Context context, final ChartOrBuilder parent, final ChartOrBuilder chartBuilder) throws IOException { 161 Objects.requireNonNull(chartBuilder); 162 if (parent == chartBuilder) { 163 throw new IllegalArgumentException("parent == chartBuilder"); 164 } 165 final MetadataOrBuilder metadata = chartBuilder.getMetadataOrBuilder(); 166 if (metadata == null) { 167 throw new IllegalArgumentException("chartBuilder", new IllegalStateException("chartBuilder.getMetadata() == null")); 168 } else if (metadata.getName() == null) { 169 throw new IllegalArgumentException("chartBuilder", new IllegalStateException("chartBuilder.getMetadata().getName() == null")); 170 } else if (metadata.getVersion() == null) { 171 throw new IllegalArgumentException("chartBuilder", new IllegalStateException("chartBuilder.getMetadata().getVersion() == null")); 172 } 173 174 if (context == null) { 175 final Map<Object, Object> map = new HashMap<>(13); 176 context = new Context() { 177 @Override 178 public final <T> T get(final Object key, final Class<T> type) { 179 Objects.requireNonNull(key); 180 Objects.requireNonNull(type); 181 return type.cast(map.get(key)); 182 } 183 184 @Override 185 public final void put(final Object key, final Object value) { 186 Objects.requireNonNull(key); 187 Objects.requireNonNull(value); 188 map.put(key, value); 189 } 190 191 @Override 192 public final boolean containsKey(final Object key) { 193 return map.containsKey(key); 194 } 195 196 @Override 197 public final void remove(final Object key) { 198 map.remove(key); 199 } 200 }; 201 } 202 203 this.beginWrite(context, parent, chartBuilder); 204 205 this.writeMetadata(context, metadata); 206 207 this.writeConfig(context, chartBuilder.getValuesOrBuilder()); 208 209 final Collection<? extends TemplateOrBuilder> templates = chartBuilder.getTemplatesOrBuilderList(); 210 if (templates != null && !templates.isEmpty()) { 211 for (final TemplateOrBuilder template : templates) { 212 this.writeTemplate(context, template); 213 } 214 } 215 216 final Collection<? extends AnyOrBuilder> files = chartBuilder.getFilesOrBuilderList(); 217 if (files != null && !files.isEmpty()) { 218 for (final AnyOrBuilder file : files) { 219 this.writeFile(context, file); 220 } 221 } 222 223 final Collection<? extends ChartOrBuilder> subcharts = chartBuilder.getDependenciesOrBuilderList(); 224 if (subcharts != null && !subcharts.isEmpty()) { 225 for (final ChartOrBuilder subchart : subcharts) { 226 if (subchart != null) { 227 this.writeSubchart(context, chartBuilder, subchart); 228 } 229 } 230 } 231 232 this.endWrite(context, parent, chartBuilder); 233 234 } 235 236 /** 237 * Creates and returns a new {@link Yaml} instance for (optional) 238 * use in writing {@link ConfigOrBuilder} and {@link 239 * MetadataOrBuilder} objects. 240 * 241 * <p>This method never returns {@code null}.</p> 242 * 243 * <p>Overrides of this method must not return {@code null}.</p> 244 * 245 * <p>Behavior is undefined if overrides of this method interact 246 * with other methods defined by this class.</p> 247 * 248 * @return a non-{@code null} {@link Yaml} instance 249 */ 250 protected Yaml createYaml() { 251 final Representer representer = new TerseRepresenter(); 252 representer.setPropertyUtils(new CustomPropertyUtils()); 253 final DumperOptions options = new DumperOptions(); 254 options.setAllowReadOnlyProperties(true); 255 return new Yaml(new SafeConstructor(), representer, options); 256 } 257 258 /** 259 * Marshals the supplied {@link Object} to YAML in the context of 260 * the supplied {@link Context} and returns the result. 261 * 262 * <p>This method never returns {@code null}.</p> 263 * 264 * <p>This method may call the {@link #createYaml()} method.</p> 265 * 266 * @param context the {@link Context} representing the write 267 * operation; must not be {@code null} 268 * 269 * @param data the {@link Object} to convert to its YAML 270 * representation; may be {@code null} 271 * 272 * @return a non-{@code null} {@link String} consisting of the 273 * appropriate YAML represesentation of the supplied {@code data} 274 * 275 * @exception IOException if a YAML serialization error occurs 276 * 277 * @exception NullPointerException if {@code context} is {@code 278 * null} 279 * 280 * @see #createYaml() 281 * 282 * @see Yaml#dumpAsMap(Object) 283 */ 284 protected final String toYAML(final Context context, final Object data) throws IOException { 285 Objects.requireNonNull(context); 286 Yaml yaml = context.get(Yaml.class.getName(), Yaml.class); 287 if (yaml == null) { 288 yaml = this.createYaml(); 289 if (yaml == null) { 290 throw new IllegalStateException("createYaml() == null"); 291 } 292 context.put(Yaml.class.getName(), yaml); 293 } 294 return yaml.dumpAsMap(data); 295 } 296 297 /** 298 * A callback method invoked when the {@link #write(Context, 299 * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} 300 * method has been invoked. 301 * 302 * <p>The default implementation of this method does nothing.</p> 303 * 304 * @param context the {@link Context} representing the write 305 * operation; must not be {@code null} 306 * 307 * @param parent the {@link ChartOrBuilder} functioning as the 308 * parent chart; may be, and often is, {@code null}; must not be 309 * identical to the {@code chartBuilder} parameter value 310 * 311 * @param chartBuilder the {@link ChartOrBuilder} to actually write; 312 * must not be {@code null}; must not be identical to the {@code 313 * parent} parameter value 314 * 315 * @exception IOException if a write error occurs 316 * 317 * @exception NullPointerException if either {@code context} or {@code chartBuilder} is {@code null} 318 * 319 * @exception IllegalArgumentException if {@code parent} is 320 * identical to {@code chartBuilder} 321 * 322 * @exception IllegalStateException if a subclass has overridden the 323 * {@link #createYaml()} method to return {@code null} and calls it 324 * from this method for some reason 325 * 326 */ 327 protected void beginWrite(final Context context, final ChartOrBuilder parent, final ChartOrBuilder chartBuilder) throws IOException { 328 Objects.requireNonNull(context); 329 Objects.requireNonNull(chartBuilder); 330 if (parent == chartBuilder) { 331 throw new IllegalArgumentException("parent == chartBuilder"); 332 } 333 } 334 335 /** 336 * A callback method invoked when the {@link #write(Context, 337 * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method has been invoked and it 338 * is time to write a relevant {@link MetadataOrBuilder} object. 339 * 340 * @param context the {@link Context} representing the write 341 * operation; must not be {@code null} 342 * 343 * @param metadata the {@link MetadataOrBuilder} to write; must not 344 * be {@code null} 345 * 346 * @exception IOException if a write error occurs 347 * 348 * @exception NullPointerException if either {@code context} or 349 * {@code metadata} is {@code null} 350 * 351 * @exception IllegalStateException if a subclass has overridden the 352 * {@link #createYaml()} method to return {@code null} and calls it 353 * from this method 354 */ 355 protected abstract void writeMetadata(final Context context, final MetadataOrBuilder metadata) throws IOException; 356 357 /** 358 * A callback method invoked when the {@link #write(Context, 359 * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method has been invoked and it 360 * is time to write a relevant {@link ConfigOrBuilder} object. 361 * 362 * @param context the {@link Context} representing the write 363 * operation; must not be {@code null} 364 * 365 * @param config the {@link ConfigOrBuilder} to write; must not 366 * be {@code null} 367 * 368 * @exception IOException if a write error occurs 369 * 370 * @exception NullPointerException if either {@code context} or 371 * {@code config} is {@code null} 372 * 373 * @exception IllegalStateException if a subclass has overridden the 374 * {@link #createYaml()} method to return {@code null} and calls it 375 * from this method 376 */ 377 protected abstract void writeConfig(final Context context, final ConfigOrBuilder config) throws IOException; 378 379 /** 380 * A callback method invoked when the {@link #write(Context, 381 * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method has been invoked and it 382 * is time to write a relevant {@link TemplateOrBuilder} object. 383 * 384 * @param context the {@link Context} representing the write 385 * operation; must not be {@code null} 386 * 387 * @param template the {@link TemplateOrBuilder} to write; must not 388 * be {@code null} 389 * 390 * @exception IOException if a write error occurs 391 * 392 * @exception NullPointerException if either {@code context} or 393 * {@code template} is {@code null} 394 * 395 * @exception IllegalStateException if a subclass has overridden the 396 * {@link #createYaml()} method to return {@code null} and calls it 397 * from this method for some reason 398 */ 399 protected abstract void writeTemplate(final Context context, final TemplateOrBuilder template) throws IOException; 400 401 /** 402 * A callback method invoked when the {@link #write(Context, 403 * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method has been invoked and it 404 * is time to write a relevant {@link AnyOrBuilder} object 405 * (representing an otherwise undifferentiated Helm chart file). 406 * 407 * @param context the {@link Context} representing the write 408 * operation; must not be {@code null} 409 * 410 * @param file the {@link AnyOrBuilder} to write; must not be {@code 411 * null} 412 * 413 * @exception IOException if a write error occurs 414 * 415 * @exception NullPointerException if either {@code context} or 416 * {@code file} is {@code null} 417 * 418 * @exception IllegalStateException if a subclass has overridden the 419 * {@link #createYaml()} method to return {@code null} and calls it 420 * from this method for some reason 421 */ 422 protected abstract void writeFile(final Context context, final AnyOrBuilder file) throws IOException; 423 424 /** 425 * A callback method invoked when the {@link #write(Context, 426 * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method has been invoked and it 427 * is time to write a relevant {@link ChartOrBuilder} object 428 * (representing a subchart within an encompassing parent Helm 429 * chart). 430 * 431 * <p>The default implementation of this method calls the {@link 432 * #write(Context, ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method.</p> 433 * 434 * @param context the {@link Context} representing the write 435 * operation; must not be {@code null} 436 * 437 * @param parent the {@link ChartOrBuilder} representing the Helm 438 * chart that parents the {@code subchart} parameter value; must not 439 * be {@code null} 440 * 441 * @param subchart the {@link ChartOrBuilder} representing the 442 * subchart to write; must not be {@code null} 443 * 444 * @exception IOException if a write error occurs 445 * 446 * @exception NullPointerException if either {@code context} or 447 * {@code parent} or {@code subchart} is {@code null} 448 * 449 * @exception IllegalArgumentException if {@code parent} is 450 * identical to {@code subchart}, or if the {@link 451 * ChartOrBuilder#getMetadata()} method returns {@code null} when 452 * invoked on either non-{@code null} {@link ChartOrBuilder}, or if 453 * the {@link MetadataOrBuilder#getName()} method returns {@code 454 * null}, or if the {@link MetadataOrBuilder#getVersion()} method 455 * returns {@code null} 456 * 457 * @exception IllegalStateException if a subclass has overridden the 458 * {@link #createYaml()} method to return {@code null} and calls it 459 * from this method for some reason 460 * 461 * @see #write(Context, ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder) 462 */ 463 protected void writeSubchart(final Context context, final ChartOrBuilder parent, final ChartOrBuilder subchart) throws IOException { 464 this.write(Objects.requireNonNull(context), Objects.requireNonNull(parent), Objects.requireNonNull(subchart)); 465 } 466 467 /** 468 * A callback method invoked when the {@link #write(Context, 469 * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method has been invoked and it 470 * is time to end the write operation. 471 * 472 * <p>The default implementation of this method does nothing.</p> 473 * 474 * @param context the {@link Context} representing the write 475 * operation; must not be {@code null} 476 * 477 * @param parent the {@link ChartOrBuilder} representing the Helm 478 * chart that parents the {@code chartBuilder} parameter value; may be, 479 * and often is, {@code null} 480 * 481 * @param chartBuilder the {@link ChartOrBuilder} representing the 482 * chart currently involved in the write operation; must not be 483 * {@code null} 484 * 485 * @exception IOException if a write error occurs 486 * 487 * @exception NullPointerException if either {@code context} or 488 * {@code chartBuilder} is {@code null} 489 * 490 * @exception IllegalArgumentException if {@code parent} is 491 * identical to {@code chartBuilder} 492 * 493 * @exception IllegalStateException if a subclass has overridden the 494 * {@link #createYaml()} method to return {@code null} and calls it 495 * from this method for some reason 496 */ 497 protected void endWrite(final Context context, final ChartOrBuilder parent, final ChartOrBuilder chartBuilder) throws IOException { 498 Objects.requireNonNull(context); 499 Objects.requireNonNull(chartBuilder); 500 if (parent == chartBuilder) { 501 throw new IllegalArgumentException("parent == chartBuilder"); 502 } 503 } 504 505 506 /* 507 * Inner and nested classes. 508 */ 509 510 511 /** 512 * A class representing the state of a write operation. 513 * 514 * @author <a href="https://about.me/lairdnelson" 515 * target="_parent">Laird Nelson</a> 516 */ 517 protected static abstract class Context { 518 519 520 /* 521 * Constructors. 522 */ 523 524 525 /** 526 * Creates a new {@link Context}. 527 */ 528 private Context() { 529 super(); 530 } 531 532 533 /* 534 * Instance methods. 535 */ 536 537 /** 538 * Returns the object indexed under the supplied {@code key}, if 539 * any, {@linkplain Class#cast(Object) cast to the proper 540 * <code>Class</code>}. 541 * 542 * <p>Implementations of this method may return {@code null}.</p> 543 * 544 * @param <T> the type of object expected 545 * 546 * @param key the key under which something is hopefully stored; 547 * may be {@code null} 548 * 549 * @param type the {@link Class} to cast the result to; must not 550 * be {@code null} 551 * 552 * @return the object in question, or {@code null} 553 * 554 * @exception NullPointerException if {@code type} is {@code null} 555 * 556 * @see #put(Object, Object) 557 */ 558 public abstract <T> T get(final Object key, final Class<T> type); 559 560 /** 561 * Stores the supplied {@code value} under the supplied {@code key}. 562 * 563 * @param key the key under which the supplied {@code value} will 564 * be stored; may be {@code null} 565 * 566 * @param value the object to store; may be {@code null} 567 * 568 * @see #get(Object, Class) 569 */ 570 public abstract void put(final Object key, final Object value); 571 572 /** 573 * Returns {@code true} if this {@link Context} implementation 574 * contains an object indexed under an {@link Object} {@linkplain 575 * Object#equals(Object) equal to} the supplied {@code key}. 576 * 577 * @param key the key in question; may be {@code null} 578 * 579 * @return {@code true} if this {@link Context} implementation 580 * contains an object indexed under an {@link Object} {@linkplain 581 * Object#equals(Object) equal to} the supplied {@code key}; 582 * {@code false} otherwise 583 */ 584 public abstract boolean containsKey(final Object key); 585 586 /** 587 * Removes any object indexed under an {@link Object} {@linkplain 588 * Object#equals(Object) equal to} the supplied {@code key}. 589 * 590 * @param key the key in question; may be {@code null} 591 */ 592 public abstract void remove(final Object key); 593 594 } 595 596 /** 597 * A {@link Representer} that attempts not to output default values 598 * or YAML tags. 599 * 600 * @author <a href="https://about.me/lairdnelson" 601 * target="_parent">Laird Nelson</a> 602 */ 603 private static final class TerseRepresenter extends Representer { 604 605 606 /* 607 * Constructors. 608 */ 609 610 611 /** 612 * Creates a new {@link TerseRepresenter}. 613 */ 614 private TerseRepresenter() { 615 super(); 616 } 617 618 619 /* 620 * Instance methods. 621 */ 622 623 624 /** 625 * Represents a Java bean normally, but without any YAML tag 626 * information. 627 * 628 * @param properties a {@link Set} of {@link Property} instances 629 * indicating what facets of the supplied {@code bean} should be 630 * represented; ignored by this implementation 631 * 632 * @param bean the {@link Object} to represent; may be {@code null} 633 * 634 * @return the result of invoking {@link 635 * Representer#representJavaBean(Set, Object)}, but after adding 636 * {@link Tag#MAP} as a {@linkplain Representer#addClassTag(Class, 637 * Tag) class tag} for the supplied {@code bean}'s class 638 */ 639 @Override 640 protected final MappingNode representJavaBean(final Set<Property> properties, final Object bean) { 641 if (bean != null) { 642 final Class<?> beanClass = bean.getClass(); 643 if (this.getTag(beanClass, null) == null) { 644 this.addClassTag(beanClass, Tag.MAP); 645 } 646 } 647 return super.representJavaBean(properties, bean); 648 } 649 650 /** 651 * Overrides the {@link 652 * Representer#representJavaBeanProperty(Object, Property, Object, 653 * Tag)} method to return {@code null} when the given property 654 * value can be omitted from its YAML representation without loss 655 * of information. 656 * 657 * @param bean the Java bean whose property value is being 658 * represented; may be {@code null} 659 * 660 * @param property the {@link Property} whose value is being 661 * represented; may be {@code null} 662 * 663 * @param value the value being represented; may be {@code null} 664 * 665 * @param tag the {@link Tag} in effect; may be {@code null} 666 * 667 * @return {@code null} or the result of invoking the {@link 668 * Representer#representJavaBeanProperty(Object, Property, Object, 669 * Tag)} method with the supplied values 670 */ 671 @Override 672 protected final NodeTuple representJavaBeanProperty(final Object bean, final Property property, final Object value, final Tag tag) { 673 final NodeTuple returnValue; 674 if (value == null || value.equals(Boolean.FALSE)) { 675 returnValue = null; 676 } else if (value instanceof CharSequence) { 677 if (((CharSequence)value).length() <= 0) { 678 returnValue = null; 679 } else { 680 returnValue = super.representJavaBeanProperty(bean, property, value, tag); 681 } 682 } else if (value instanceof Collection) { 683 if (((Collection<?>)value).isEmpty()) { 684 returnValue = null; 685 } else { 686 returnValue = super.representJavaBeanProperty(bean, property, value, tag); 687 } 688 } else if (value instanceof Map) { 689 if (((Map<?, ?>)value).isEmpty()) { 690 returnValue = null; 691 } else { 692 returnValue = super.representJavaBeanProperty(bean, property, value, tag); 693 } 694 } else if (value.getClass().isArray()) { 695 if (Array.getLength(value) <= 0) { 696 returnValue = null; 697 } else { 698 returnValue = super.representJavaBeanProperty(bean, property, value, tag); 699 } 700 } else { 701 returnValue = super.representJavaBeanProperty(bean, property, value, tag); 702 } 703 return returnValue; 704 } 705 706 } 707 708 /** 709 * A {@link PropertyUtils} that knows how to represent certain 710 * properties of certain Helm-related objects for the purposes of 711 * serialization to YAML. 712 * 713 * @author <a href="https://about.me/lairdnelson" 714 * target="_parent">Laird Nelson</a> 715 */ 716 private static final class CustomPropertyUtils extends PropertyUtils { 717 718 719 /* 720 * Constructors. 721 */ 722 723 724 /** 725 * Creates a new {@link CustomPropertyUtils}. 726 */ 727 private CustomPropertyUtils() { 728 super(); 729 } 730 731 732 /* 733 * Instance methods. 734 */ 735 736 737 /** 738 * Returns a {@link Set} of {@link Property} instances that will 739 * represent Java objects of the supplied {@code type} during YAML 740 * serialization. 741 * 742 * <p>This implementation overrides the {@link 743 * PropertyUtils#createPropertySet(Class, BeanAccess)} method to 744 * build explicit representations for {@link MetadataOrBuilder}, 745 * {@link MaintainerOrBuilder} and {@link ConfigOrBuilder} 746 * interfaces.</p> 747 * 748 * @param type a {@link Class} for which a {@link Set} of 749 * representational {@link Property} instances should be returned; 750 * may be {@code null} 751 * 752 * @param beanAccess ignored by this implementation 753 * 754 * @return a {@link Set} of {@link Property} instances; never 755 * {@code null} 756 */ 757 @Override 758 protected final Set<Property> createPropertySet(final Class<?> type, final BeanAccess beanAccess) { 759 final Set<Property> returnValue; 760 if (MetadataOrBuilder.class.isAssignableFrom(type)) { 761 returnValue = new TreeSet<>(); 762 try { 763 returnValue.add(new MethodProperty(new PropertyDescriptor("annotations", type, "getAnnotationsMap", null))); 764 returnValue.add(new MethodProperty(new PropertyDescriptor("apiVersion", type, "getApiVersion", null))); 765 returnValue.add(new MethodProperty(new PropertyDescriptor("appVersion", type, "getAppVersion", null))); 766 returnValue.add(new MethodProperty(new PropertyDescriptor("condition", type, "getCondition", null))); 767 returnValue.add(new MethodProperty(new PropertyDescriptor("deprecated", type, "getDeprecated", null))); 768 returnValue.add(new MethodProperty(new PropertyDescriptor("description", type, "getDescription", null))); 769 returnValue.add(new MethodProperty(new PropertyDescriptor("engine", type, "getEngine", null))); 770 returnValue.add(new MethodProperty(new PropertyDescriptor("home", type, "getHome", null))); 771 returnValue.add(new MethodProperty(new PropertyDescriptor("icon", type, "getIcon", null))); 772 returnValue.add(new MethodProperty(new PropertyDescriptor("keywords", type, "getKeywordsList", null))); 773 returnValue.add(new MethodProperty(new PropertyDescriptor("kubeVersion", type, "getKubeVersion", null))); 774 returnValue.add(new MethodProperty(new PropertyDescriptor("maintainers", type, "getMaintainersOrBuilderList", null))); 775 returnValue.add(new MethodProperty(new PropertyDescriptor("name", type, "getName", null))); 776 returnValue.add(new MethodProperty(new PropertyDescriptor("sources", type, "getSourcesList", null))); 777 returnValue.add(new MethodProperty(new PropertyDescriptor("tags", type, "getTags", null))); 778 returnValue.add(new MethodProperty(new PropertyDescriptor("tillerVersion", type, "getTillerVersion", null))); 779 returnValue.add(new MethodProperty(new PropertyDescriptor("version", type, "getVersion", null))); 780 } catch (final IntrospectionException introspectionException) { 781 throw new IllegalStateException(introspectionException.getMessage(), introspectionException); 782 } 783 } else if (MaintainerOrBuilder.class.isAssignableFrom(type)) { 784 returnValue = new TreeSet<>(); 785 try { 786 returnValue.add(new MethodProperty(new PropertyDescriptor("email", type, "getEmail", null))); 787 returnValue.add(new MethodProperty(new PropertyDescriptor("name", type, "getName", null))); 788 returnValue.add(new MethodProperty(new PropertyDescriptor("url", type, "getUrl", null))); 789 } catch (final IntrospectionException introspectionException) { 790 throw new IllegalStateException(introspectionException.getMessage(), introspectionException); 791 } 792 } else if (ConfigOrBuilder.class.isAssignableFrom(type)) { 793 returnValue = new TreeSet<>(); 794 try { 795 returnValue.add(new MethodProperty(new PropertyDescriptor("raw", type, "getRaw", null))); 796 } catch (final IntrospectionException introspectionException) { 797 throw new IllegalStateException(introspectionException.getMessage(), introspectionException); 798 } 799 } else { 800 returnValue = super.createPropertySet(type, beanAccess); 801 } 802 return returnValue; 803 } 804 805 } 806 807}