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}