/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- * * Copyright © 2017-2018 microBean. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. */ package org.microbean.helm.chart; import java.beans.IntrospectionException; import java.beans.PropertyDescriptor; import java.io.Closeable; import java.io.IOException; import java.lang.reflect.Array; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.TreeSet; import java.util.Set; import com.google.protobuf.AnyOrBuilder; import hapi.chart.ChartOuterClass.ChartOrBuilder; import hapi.chart.ConfigOuterClass.ConfigOrBuilder; import hapi.chart.MetadataOuterClass.MaintainerOrBuilder; import hapi.chart.MetadataOuterClass.MetadataOrBuilder; import hapi.chart.TemplateOuterClass.TemplateOrBuilder; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.introspector.BeanAccess; import org.yaml.snakeyaml.introspector.MethodProperty; import org.yaml.snakeyaml.introspector.Property; import org.yaml.snakeyaml.introspector.PropertyUtils; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.NodeTuple; import org.yaml.snakeyaml.nodes.Tag; import org.yaml.snakeyaml.representer.Representer; /** * An object capable of writing or serializing or otherwise * representing a {@link ChartOrBuilder}. * * @author <a href="https://about.me/lairdnelson" * target="_parent">Laird Nelson</a> * * @see #write(ChartOuterClass.ChartOrBuilder) */ public abstract class AbstractChartWriter implements Closeable { /* * Constructors. */ /** * Creates a new {@link AbstractChartWriter}. */ protected AbstractChartWriter() { super(); } /* * Instance methods. */ /** * Writes or serializes or otherwise represents the supplied {@link * ChartOrBuilder}. * * @param chartBuilder the {@link ChartOrBuilder} to write; must not * be {@code null} * * @exception IOException if a write error occurs * * @exception NullPointerException if {@code chartBuilder} is {@code * null} * * @exception IllegalArgumentException if the {@link * ChartOrBuilder#getMetadata()} method returns {@code null}, or if * the {@link MetadataOrBuilder#getName()} method returns {@code * null}, or if the {@link MetadataOrBuilder#getVersion()} method * returns {@code null} * * @exception IllegalStateException if a subclass has overridden the * {@link #createYaml()} method to return {@code null} and calls it * * @see #write(Context, ChartOuterClass.ChartOrBuilder, * ChartOuterClass.ChartOrBuilder) */ public final void write(final ChartOrBuilder chartBuilder) throws IOException { this.write(null, null, Objects.requireNonNull(chartBuilder)); } /** * Writes or serializes or otherwise represents the supplied {@code * chartBuilder} as a subchart of the supplied {@code parent} (which * may be, and often is, {@code null}). * * @param context the {@link Context} representing the write * operation; may be {@code null} * * @param parent the {@link ChartOrBuilder} functioning as the * parent chart; may be, and often is, {@code null}; must not be * identical to the {@code chartBuilder} parameter value * * @param chartBuilder the {@link ChartOrBuilder} to actually write; * must not be {@code null}; must not be identical to the {@code * parent} parameter value * * @exception IOException if a write error occurs * * @exception NullPointerException if {@code chartBuilder} is {@code null} * * @exception IllegalArgumentException if {@code parent} is * identical to {@code chartBuilder}, or if the {@link * ChartOrBuilder#getMetadata()} method returns {@code null}, or if * the {@link MetadataOrBuilder#getName()} method returns {@code * null}, or if the {@link MetadataOrBuilder#getVersion()} method * returns {@code null} * * @exception IllegalStateException if a subclass has overridden the * {@link #createYaml()} method to return {@code null} and calls it * * @see #beginWrite(Context, ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder) * * @see #writeMetadata(Context, MetadataOuterClass.MetadataOrBuilder) * * @see #writeConfig(Context, ConfigOuterClass.ConfigOrBuilder) * * @see #writeTemplate(Context, TemplateOuterClass.TemplateOrBuilder) * * @see #writeFile(Context, AnyOrBuilder) * * @see #writeSubchart(Context, ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder) * * @see #endWrite(Context, ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder) */ protected void write(Context context, final ChartOrBuilder parent, final ChartOrBuilder chartBuilder) throws IOException { Objects.requireNonNull(chartBuilder); if (parent == chartBuilder) { throw new IllegalArgumentException("parent == chartBuilder"); } final MetadataOrBuilder metadata = chartBuilder.getMetadataOrBuilder(); if (metadata == null) { throw new IllegalArgumentException("chartBuilder", new IllegalStateException("chartBuilder.getMetadata() == null")); } else if (metadata.getName() == null) { throw new IllegalArgumentException("chartBuilder", new IllegalStateException("chartBuilder.getMetadata().getName() == null")); } else if (metadata.getVersion() == null) { throw new IllegalArgumentException("chartBuilder", new IllegalStateException("chartBuilder.getMetadata().getVersion() == null")); } if (context == null) { final Map<Object, Object> map = new HashMap<>(13); context = new Context() { @Override public final <T> T get(final Object key, final Class<T> type) { Objects.requireNonNull(key); Objects.requireNonNull(type); return type.cast(map.get(key)); } @Override public final void put(final Object key, final Object value) { Objects.requireNonNull(key); Objects.requireNonNull(value); map.put(key, value); } @Override public final boolean containsKey(final Object key) { return map.containsKey(key); } @Override public final void remove(final Object key) { map.remove(key); } }; } this.beginWrite(context, parent, chartBuilder); this.writeMetadata(context, metadata); this.writeConfig(context, chartBuilder.getValuesOrBuilder()); final Collection<? extends TemplateOrBuilder> templates = chartBuilder.getTemplatesOrBuilderList(); if (templates != null && !templates.isEmpty()) { for (final TemplateOrBuilder template : templates) { this.writeTemplate(context, template); } } final Collection<? extends AnyOrBuilder> files = chartBuilder.getFilesOrBuilderList(); if (files != null && !files.isEmpty()) { for (final AnyOrBuilder file : files) { this.writeFile(context, file); } } final Collection<? extends ChartOrBuilder> subcharts = chartBuilder.getDependenciesOrBuilderList(); if (subcharts != null && !subcharts.isEmpty()) { for (final ChartOrBuilder subchart : subcharts) { if (subchart != null) { this.writeSubchart(context, chartBuilder, subchart); } } } this.endWrite(context, parent, chartBuilder); } /** * Creates and returns a new {@link Yaml} instance for (optional) * use in writing {@link ConfigOrBuilder} and {@link * MetadataOrBuilder} objects. * * <p>This method never returns {@code null}.</p> * * <p>Overrides of this method must not return {@code null}.</p> * * <p>Behavior is undefined if overrides of this method interact * with other methods defined by this class.</p> * * @return a non-{@code null} {@link Yaml} instance */ protected Yaml createYaml() { final Representer representer = new TerseRepresenter(); representer.setPropertyUtils(new CustomPropertyUtils()); final DumperOptions options = new DumperOptions(); options.setAllowReadOnlyProperties(true); return new Yaml(new SafeConstructor(), representer, options); } /** * Marshals the supplied {@link Object} to YAML in the context of * the supplied {@link Context} and returns the result. * * <p>This method never returns {@code null}.</p> * * <p>This method may call the {@link #createYaml()} method.</p> * * @param context the {@link Context} representing the write * operation; must not be {@code null} * * @param data the {@link Object} to convert to its YAML * representation; may be {@code null} * * @return a non-{@code null} {@link String} consisting of the * appropriate YAML represesentation of the supplied {@code data} * * @exception IOException if a YAML serialization error occurs * * @exception NullPointerException if {@code context} is {@code * null} * * @see #createYaml() * * @see Yaml#dumpAsMap(Object) */ protected final String toYAML(final Context context, final Object data) throws IOException { Objects.requireNonNull(context); Yaml yaml = context.get(Yaml.class.getName(), Yaml.class); if (yaml == null) { yaml = this.createYaml(); if (yaml == null) { throw new IllegalStateException("createYaml() == null"); } context.put(Yaml.class.getName(), yaml); } return yaml.dumpAsMap(data); } /** * A callback method invoked when the {@link #write(Context, * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} * method has been invoked. * * <p>The default implementation of this method does nothing.</p> * * @param context the {@link Context} representing the write * operation; must not be {@code null} * * @param parent the {@link ChartOrBuilder} functioning as the * parent chart; may be, and often is, {@code null}; must not be * identical to the {@code chartBuilder} parameter value * * @param chartBuilder the {@link ChartOrBuilder} to actually write; * must not be {@code null}; must not be identical to the {@code * parent} parameter value * * @exception IOException if a write error occurs * * @exception NullPointerException if either {@code context} or {@code chartBuilder} is {@code null} * * @exception IllegalArgumentException if {@code parent} is * identical to {@code chartBuilder} * * @exception IllegalStateException if a subclass has overridden the * {@link #createYaml()} method to return {@code null} and calls it * from this method for some reason * */ protected void beginWrite(final Context context, final ChartOrBuilder parent, final ChartOrBuilder chartBuilder) throws IOException { Objects.requireNonNull(context); Objects.requireNonNull(chartBuilder); if (parent == chartBuilder) { throw new IllegalArgumentException("parent == chartBuilder"); } } /** * A callback method invoked when the {@link #write(Context, * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method has been invoked and it * is time to write a relevant {@link MetadataOrBuilder} object. * * @param context the {@link Context} representing the write * operation; must not be {@code null} * * @param metadata the {@link MetadataOrBuilder} to write; must not * be {@code null} * * @exception IOException if a write error occurs * * @exception NullPointerException if either {@code context} or * {@code metadata} is {@code null} * * @exception IllegalStateException if a subclass has overridden the * {@link #createYaml()} method to return {@code null} and calls it * from this method */ protected abstract void writeMetadata(final Context context, final MetadataOrBuilder metadata) throws IOException; /** * A callback method invoked when the {@link #write(Context, * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method has been invoked and it * is time to write a relevant {@link ConfigOrBuilder} object. * * @param context the {@link Context} representing the write * operation; must not be {@code null} * * @param config the {@link ConfigOrBuilder} to write; must not * be {@code null} * * @exception IOException if a write error occurs * * @exception NullPointerException if either {@code context} or * {@code config} is {@code null} * * @exception IllegalStateException if a subclass has overridden the * {@link #createYaml()} method to return {@code null} and calls it * from this method */ protected abstract void writeConfig(final Context context, final ConfigOrBuilder config) throws IOException; /** * A callback method invoked when the {@link #write(Context, * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method has been invoked and it * is time to write a relevant {@link TemplateOrBuilder} object. * * @param context the {@link Context} representing the write * operation; must not be {@code null} * * @param template the {@link TemplateOrBuilder} to write; must not * be {@code null} * * @exception IOException if a write error occurs * * @exception NullPointerException if either {@code context} or * {@code template} is {@code null} * * @exception IllegalStateException if a subclass has overridden the * {@link #createYaml()} method to return {@code null} and calls it * from this method for some reason */ protected abstract void writeTemplate(final Context context, final TemplateOrBuilder template) throws IOException; /** * A callback method invoked when the {@link #write(Context, * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method has been invoked and it * is time to write a relevant {@link AnyOrBuilder} object * (representing an otherwise undifferentiated Helm chart file). * * @param context the {@link Context} representing the write * operation; must not be {@code null} * * @param file the {@link AnyOrBuilder} to write; must not be {@code * null} * * @exception IOException if a write error occurs * * @exception NullPointerException if either {@code context} or * {@code file} is {@code null} * * @exception IllegalStateException if a subclass has overridden the * {@link #createYaml()} method to return {@code null} and calls it * from this method for some reason */ protected abstract void writeFile(final Context context, final AnyOrBuilder file) throws IOException; /** * A callback method invoked when the {@link #write(Context, * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method has been invoked and it * is time to write a relevant {@link ChartOrBuilder} object * (representing a subchart within an encompassing parent Helm * chart). * * <p>The default implementation of this method calls the {@link * #write(Context, ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method.</p> * * @param context the {@link Context} representing the write * operation; must not be {@code null} * * @param parent the {@link ChartOrBuilder} representing the Helm * chart that parents the {@code subchart} parameter value; must not * be {@code null} * * @param subchart the {@link ChartOrBuilder} representing the * subchart to write; must not be {@code null} * * @exception IOException if a write error occurs * * @exception NullPointerException if either {@code context} or * {@code parent} or {@code subchart} is {@code null} * * @exception IllegalArgumentException if {@code parent} is * identical to {@code subchart}, or if the {@link * ChartOrBuilder#getMetadata()} method returns {@code null} when * invoked on either non-{@code null} {@link ChartOrBuilder}, or if * the {@link MetadataOrBuilder#getName()} method returns {@code * null}, or if the {@link MetadataOrBuilder#getVersion()} method * returns {@code null} * * @exception IllegalStateException if a subclass has overridden the * {@link #createYaml()} method to return {@code null} and calls it * from this method for some reason * * @see #write(Context, ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder) */ protected void writeSubchart(final Context context, final ChartOrBuilder parent, final ChartOrBuilder subchart) throws IOException { this.write(Objects.requireNonNull(context), Objects.requireNonNull(parent), Objects.requireNonNull(subchart)); } /** * A callback method invoked when the {@link #write(Context, * ChartOuterClass.ChartOrBuilder, ChartOuterClass.ChartOrBuilder)} method has been invoked and it * is time to end the write operation. * * <p>The default implementation of this method does nothing.</p> * * @param context the {@link Context} representing the write * operation; must not be {@code null} * * @param parent the {@link ChartOrBuilder} representing the Helm * chart that parents the {@code chartBuilder} parameter value; may be, * and often is, {@code null} * * @param chartBuilder the {@link ChartOrBuilder} representing the * chart currently involved in the write operation; must not be * {@code null} * * @exception IOException if a write error occurs * * @exception NullPointerException if either {@code context} or * {@code chartBuilder} is {@code null} * * @exception IllegalArgumentException if {@code parent} is * identical to {@code chartBuilder} * * @exception IllegalStateException if a subclass has overridden the * {@link #createYaml()} method to return {@code null} and calls it * from this method for some reason */ protected void endWrite(final Context context, final ChartOrBuilder parent, final ChartOrBuilder chartBuilder) throws IOException { Objects.requireNonNull(context); Objects.requireNonNull(chartBuilder); if (parent == chartBuilder) { throw new IllegalArgumentException("parent == chartBuilder"); } } /* * Inner and nested classes. */ /** * A class representing the state of a write operation. * * @author <a href="https://about.me/lairdnelson" * target="_parent">Laird Nelson</a> */ protected static abstract class Context { /* * Constructors. */ /** * Creates a new {@link Context}. */ private Context() { super(); } /* * Instance methods. */ /** * Returns the object indexed under the supplied {@code key}, if * any, {@linkplain Class#cast(Object) cast to the proper * <code>Class</code>}. * * <p>Implementations of this method may return {@code null}.</p> * * @param <T> the type of object expected * * @param key the key under which something is hopefully stored; * may be {@code null} * * @param type the {@link Class} to cast the result to; must not * be {@code null} * * @return the object in question, or {@code null} * * @exception NullPointerException if {@code type} is {@code null} * * @see #put(Object, Object) */ public abstract <T> T get(final Object key, final Class<T> type); /** * Stores the supplied {@code value} under the supplied {@code key}. * * @param key the key under which the supplied {@code value} will * be stored; may be {@code null} * * @param value the object to store; may be {@code null} * * @see #get(Object, Class) */ public abstract void put(final Object key, final Object value); /** * Returns {@code true} if this {@link Context} implementation * contains an object indexed under an {@link Object} {@linkplain * Object#equals(Object) equal to} the supplied {@code key}. * * @param key the key in question; may be {@code null} * * @return {@code true} if this {@link Context} implementation * contains an object indexed under an {@link Object} {@linkplain * Object#equals(Object) equal to} the supplied {@code key}; * {@code false} otherwise */ public abstract boolean containsKey(final Object key); /** * Removes any object indexed under an {@link Object} {@linkplain * Object#equals(Object) equal to} the supplied {@code key}. * * @param key the key in question; may be {@code null} */ public abstract void remove(final Object key); } /** * A {@link Representer} that attempts not to output default values * or YAML tags. * * @author <a href="https://about.me/lairdnelson" * target="_parent">Laird Nelson</a> */ private static final class TerseRepresenter extends Representer { /* * Constructors. */ /** * Creates a new {@link TerseRepresenter}. */ private TerseRepresenter() { super(); } /* * Instance methods. */ /** * Represents a Java bean normally, but without any YAML tag * information. * * @param properties a {@link Set} of {@link Property} instances * indicating what facets of the supplied {@code bean} should be * represented; ignored by this implementation * * @param bean the {@link Object} to represent; may be {@code null} * * @return the result of invoking {@link * Representer#representJavaBean(Set, Object)}, but after adding * {@link Tag#MAP} as a {@linkplain Representer#addClassTag(Class, * Tag) class tag} for the supplied {@code bean}'s class */ @Override protected final MappingNode representJavaBean(final Set<Property> properties, final Object bean) { if (bean != null) { final Class<?> beanClass = bean.getClass(); if (this.getTag(beanClass, null) == null) { this.addClassTag(beanClass, Tag.MAP); } } return super.representJavaBean(properties, bean); } /** * Overrides the {@link * Representer#representJavaBeanProperty(Object, Property, Object, * Tag)} method to return {@code null} when the given property * value can be omitted from its YAML representation without loss * of information. * * @param bean the Java bean whose property value is being * represented; may be {@code null} * * @param property the {@link Property} whose value is being * represented; may be {@code null} * * @param value the value being represented; may be {@code null} * * @param tag the {@link Tag} in effect; may be {@code null} * * @return {@code null} or the result of invoking the {@link * Representer#representJavaBeanProperty(Object, Property, Object, * Tag)} method with the supplied values */ @Override protected final NodeTuple representJavaBeanProperty(final Object bean, final Property property, final Object value, final Tag tag) { final NodeTuple returnValue; if (value == null || value.equals(Boolean.FALSE)) { returnValue = null; } else if (value instanceof CharSequence) { if (((CharSequence)value).length() <= 0) { returnValue = null; } else { returnValue = super.representJavaBeanProperty(bean, property, value, tag); } } else if (value instanceof Collection) { if (((Collection<?>)value).isEmpty()) { returnValue = null; } else { returnValue = super.representJavaBeanProperty(bean, property, value, tag); } } else if (value instanceof Map) { if (((Map<?, ?>)value).isEmpty()) { returnValue = null; } else { returnValue = super.representJavaBeanProperty(bean, property, value, tag); } } else if (value.getClass().isArray()) { if (Array.getLength(value) <= 0) { returnValue = null; } else { returnValue = super.representJavaBeanProperty(bean, property, value, tag); } } else { returnValue = super.representJavaBeanProperty(bean, property, value, tag); } return returnValue; } } /** * A {@link PropertyUtils} that knows how to represent certain * properties of certain Helm-related objects for the purposes of * serialization to YAML. * * @author <a href="https://about.me/lairdnelson" * target="_parent">Laird Nelson</a> */ private static final class CustomPropertyUtils extends PropertyUtils { /* * Constructors. */ /** * Creates a new {@link CustomPropertyUtils}. */ private CustomPropertyUtils() { super(); } /* * Instance methods. */ /** * Returns a {@link Set} of {@link Property} instances that will * represent Java objects of the supplied {@code type} during YAML * serialization. * * <p>This implementation overrides the {@link * PropertyUtils#createPropertySet(Class, BeanAccess)} method to * build explicit representations for {@link MetadataOrBuilder}, * {@link MaintainerOrBuilder} and {@link ConfigOrBuilder} * interfaces.</p> * * @param type a {@link Class} for which a {@link Set} of * representational {@link Property} instances should be returned; * may be {@code null} * * @param beanAccess ignored by this implementation * * @return a {@link Set} of {@link Property} instances; never * {@code null} */ @Override protected final Set<Property> createPropertySet(final Class<?> type, final BeanAccess beanAccess) { final Set<Property> returnValue; if (MetadataOrBuilder.class.isAssignableFrom(type)) { returnValue = new TreeSet<>(); try { returnValue.add(new MethodProperty(new PropertyDescriptor("annotations", type, "getAnnotationsMap", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("apiVersion", type, "getApiVersion", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("appVersion", type, "getAppVersion", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("condition", type, "getCondition", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("deprecated", type, "getDeprecated", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("description", type, "getDescription", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("engine", type, "getEngine", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("home", type, "getHome", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("icon", type, "getIcon", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("keywords", type, "getKeywordsList", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("kubeVersion", type, "getKubeVersion", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("maintainers", type, "getMaintainersOrBuilderList", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("name", type, "getName", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("sources", type, "getSourcesList", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("tags", type, "getTags", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("tillerVersion", type, "getTillerVersion", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("version", type, "getVersion", null))); } catch (final IntrospectionException introspectionException) { throw new IllegalStateException(introspectionException.getMessage(), introspectionException); } } else if (MaintainerOrBuilder.class.isAssignableFrom(type)) { returnValue = new TreeSet<>(); try { returnValue.add(new MethodProperty(new PropertyDescriptor("email", type, "getEmail", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("name", type, "getName", null))); returnValue.add(new MethodProperty(new PropertyDescriptor("url", type, "getUrl", null))); } catch (final IntrospectionException introspectionException) { throw new IllegalStateException(introspectionException.getMessage(), introspectionException); } } else if (ConfigOrBuilder.class.isAssignableFrom(type)) { returnValue = new TreeSet<>(); try { returnValue.add(new MethodProperty(new PropertyDescriptor("raw", type, "getRaw", null))); } catch (final IntrospectionException introspectionException) { throw new IllegalStateException(introspectionException.getMessage(), introspectionException); } } else { returnValue = super.createPropertySet(type, beanAccess); } return returnValue; } } }