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.io.IOException;
020
021import java.util.Map;
022import java.util.Objects;
023
024import com.google.protobuf.AnyOrBuilder;
025import com.google.protobuf.ByteString;
026
027import hapi.chart.ChartOuterClass.ChartOrBuilder;
028import hapi.chart.ConfigOuterClass.ConfigOrBuilder;
029import hapi.chart.MetadataOuterClass.MetadataOrBuilder;
030import hapi.chart.TemplateOuterClass.TemplateOrBuilder;
031
032/**
033 * A partial {@link AbstractChartWriter} whose implementations save
034 * {@link ChartOrBuilder} objects to a destination that can
035 * be considered an archive of some sort.
036 *
037 * @author <a href="https://about.me/lairdnelson"
038 * target="_parent">Laird Nelson</a>
039 */
040public abstract class AbstractArchiveChartWriter extends AbstractChartWriter {
041
042
043  /*
044   * Constructors.
045   */
046
047
048  /**
049   * Creates a new {@link AbstractArchiveChartWriter}.
050   */
051  protected AbstractArchiveChartWriter() {
052    super();
053  }
054
055
056  /*
057   * Instance methods.
058   */
059
060
061  /**
062   * {@inheritDoc}
063   *
064   * <p>The {@link AbstractArchiveChartWriter} implementation stores a
065   * {@link String} representing the required path layout under a
066   * "{@code path}" key in the supplied {@link Context}.</p>
067   */
068  @Override
069  protected void beginWrite(final Context context, final ChartOrBuilder parent, final ChartOrBuilder chartBuilder) throws IOException {
070    Objects.requireNonNull(context);
071    Objects.requireNonNull(chartBuilder);
072    if (parent == chartBuilder) {
073      throw new IllegalArgumentException("parent == chartBuilder");
074    }
075    final MetadataOrBuilder metadata = chartBuilder.getMetadataOrBuilder();
076    if (metadata == null) {
077      throw new IllegalArgumentException("chartBuilder", new IllegalStateException("chartBuilder.getMetadata() == null"));
078    }
079    final String chartName = metadata.getName();
080    if (chartName == null) {
081      throw new IllegalArgumentException("chartBuilder", new IllegalStateException("chartBuilder.getMetadata().getName() == null"));
082    }
083    
084    if (parent == null) {
085      context.put("path", new StringBuilder(chartName).append("/").toString());
086    } else {
087      final MetadataOrBuilder parentMetadata = parent.getMetadataOrBuilder();
088      if (parentMetadata == null) {
089        throw new IllegalArgumentException("parent", new IllegalStateException("parent.getMetadata() == null"));
090      }
091      final String parentChartName = parentMetadata.getName();
092      if (parentChartName == null) {
093        throw new IllegalArgumentException("parent", new IllegalStateException("parent.getMetadata().getName() == null"));
094      }      
095      context.put("path", new StringBuilder(context.get("path", String.class)).append("charts/").append(chartName).append("/").toString());
096    }
097  }
098
099  /**
100   * {@inheritDoc}
101   *
102   * <p>The {@link AbstractArchiveChartWriter} implementation writes
103   * the {@linkplain #toYAML(Context, Object) YAML representation} of
104   * the supplied {@link MetadataOrBuilder} to an appropriate archive
105   * entry named {@code Chart.yaml} within the current chart path.</p>
106   *
107   * @exception NullPointerException if either {@code context} or
108   * {@code metadata} is {@code null}
109   */
110  @Override
111  protected void writeMetadata(final Context context, final MetadataOrBuilder metadata) throws IOException {
112    Objects.requireNonNull(context);
113    Objects.requireNonNull(metadata);
114
115    final String yaml = this.toYAML(context, metadata);
116    this.writeEntry(context, "Chart.yaml", yaml);
117  }
118
119  /**
120   * {@inheritDoc}
121   *
122   * <p>This implementation writes the {@linkplain #toYAML(Context,
123   * Object) YAML representation} of the supplied {@link
124   * ConfigOrBuilder} to an appropriate archive entry named {@code
125   * values.yaml} within the current chart path.</p>
126   *
127   * @exception NullPointerException if {@code context} is {@code
128   * null}
129   */
130  @Override
131  protected void writeConfig(final Context context, final ConfigOrBuilder config) throws IOException {
132    Objects.requireNonNull(context);
133    
134    if (config != null) {
135      final String raw = config.getRaw();
136      final String yaml;
137      if (raw == null || raw.isEmpty()) {
138        final Map<String, Object> valuesMap = Configs.toMap(config);
139        if (valuesMap == null || valuesMap.isEmpty()) {
140          yaml = "";
141        } else {
142          yaml = this.toYAML(context, valuesMap);
143        }
144      } else {
145        yaml = raw;
146      }
147      this.writeEntry(context, "values.yaml", yaml);
148    }
149  }
150
151  /**
152   * {@inheritDoc}
153   *
154   * <p>This implementation writes the {@linkplain
155   * TemplateOrBuilder#getData() data} of the supplied {@link
156   * TemplateOrBuilder} to an appropriate archive entry named in part
157   * by the return value of the {@link TemplateOrBuilder#getName()}
158   * method within the current chart path.</p>
159   *
160   * @exception NullPointerException if {@code context} is {@code
161   * null}
162   */
163  @Override
164  protected void writeTemplate(final Context context, final TemplateOrBuilder template) throws IOException {
165    Objects.requireNonNull(context);
166    
167    if (template != null) {
168      final String templateName = template.getName();
169      if (templateName != null && !templateName.isEmpty()) {
170        final ByteString data = template.getData();
171        if (data != null && data.size() > 0) {
172          final String dataString = data.toStringUtf8();
173          assert dataString != null;
174          assert !dataString.isEmpty();
175          this.writeEntry(context, templateName, dataString);
176        }
177      }
178    }
179  }
180
181  /**
182   * {@inheritDoc}
183   *
184   * <p>This implementation writes the {@linkplain
185   * AnyOrBuilder#getValue() contents} of the supplied {@link
186   * AnyOrBuilder} to an appropriate archive entry named in part by
187   * the return value of the {@link AnyOrBuilder#getTypeUrl()} method
188   * within the current chart path.</p>
189   *
190   * @exception NullPointerException if {@code context} is {@code
191   * null}
192   */
193  @Override
194  protected void writeFile(final Context context, final AnyOrBuilder file) throws IOException {
195    Objects.requireNonNull(context);
196    
197    if (file != null) {
198      final String fileName = file.getTypeUrl();
199      if (fileName != null && !fileName.isEmpty()) {
200        final ByteString data = file.getValue();
201        if (data != null && data.size() > 0) {
202          final String dataString = data.toStringUtf8();
203          assert dataString != null;
204          assert !dataString.isEmpty();
205          this.writeEntry(context, fileName, dataString);
206        }
207      }
208    }
209  }
210
211  /**
212   * {@inheritDoc}
213   *
214   * <p>This implementation ensures that the current chart path,
215   * residing under the "{@code path}" key in the supplied {@link
216   * AbstractChartWriter.Context}, is reset properly.</p>
217   *
218   * @exception NullPointerException if either {@code context} or
219   * {@code chartBuilder} is {@code null}
220   */
221  @Override
222  protected void endWrite(final Context context, final ChartOrBuilder parent, final ChartOrBuilder chartBuilder) throws IOException {
223    Objects.requireNonNull(context);
224    Objects.requireNonNull(chartBuilder);
225    if (chartBuilder == parent) {
226      throw new IllegalArgumentException("chartBuilder == parent");
227    }
228    
229    if (parent == null) {
230      context.remove("path");
231    } else {
232      final String path = context.get("path", String.class);
233      assert path != null;
234      final int chartsIndex = path.lastIndexOf("/charts/");
235      assert chartsIndex > 0;
236      context.put("path", path.substring(0, chartsIndex + 1));
237    }
238  }
239
240  /**
241   * Writes the supplied {@code contents} to an appropriate archive
242   * entry that is expected to be suffixed with the supplied {@code
243   * path} in the context of the write operation described by the
244   * supplied {@link Context}.
245   *
246   * @param context the {@link Context} describing the write operation
247   * in effect; must not be {@code null}
248   *
249   * @param path the path within an abstract archive to write;
250   * interpreted as being relative to the current notional chart path,
251   * whatever that might be; must not be {@code null} or {@linkplain
252   * String#isEmpty() empty}
253   *
254   * @param contents the contents to write; must not be {@code null}
255   *
256   * @exception IOException if a write error occurs
257   *
258   * @exception NullPointerException if {@code context}, {@code path}
259   * or {@code contents} is {@code null}
260   *
261   * @exception IllegalArgumentException if {@code path} {@linkplain
262   * String#isEmpty() is empty}
263   */
264  protected abstract void writeEntry(final Context context, final String path, final String contents) throws IOException;
265
266}