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.maven;
018
019import java.io.BufferedOutputStream;
020import java.io.Closeable;
021import java.io.IOException;
022import java.io.OutputStream;
023
024import java.net.UnknownServiceException;
025import java.net.URI;
026import java.net.URISyntaxException;
027import java.net.URL;
028import java.net.URLConnection;
029
030import java.nio.file.Files;
031import java.nio.file.Path;
032import java.nio.file.Paths;
033
034import java.util.Objects;
035
036import java.util.zip.GZIPOutputStream;
037
038import javax.inject.Inject;
039
040import hapi.chart.ChartOuterClass.Chart;
041import hapi.chart.MetadataOuterClass.MetadataOrBuilder;
042
043import org.apache.maven.model.Build;
044
045import org.apache.maven.project.MavenProject;
046
047import org.apache.maven.plugin.MojoExecutionException;
048import org.apache.maven.plugin.MojoFailureException;
049
050import org.apache.maven.plugin.logging.Log;
051
052import org.apache.maven.plugins.annotations.Mojo;
053import org.apache.maven.plugins.annotations.Parameter;
054
055import org.codehaus.plexus.personality.plexus.lifecycle.phase.Disposable;
056
057import org.microbean.helm.chart.AbstractChartLoader;
058import org.microbean.helm.chart.AbstractChartWriter;
059import org.microbean.helm.chart.URLChartLoader;
060import org.microbean.helm.chart.TapeArchiveChartWriter;
061
062/**
063 * Packages a Helm chart from its raw materials.
064 *
065 * @author <a href="https://about.me/lairdnelson"
066 * target="_parent">Laird Nelson</a>
067 *
068 * @see #execute()
069 */
070@Mojo(name = "package")
071public class PackageMojo extends AbstractHelmMojo implements Disposable {
072
073
074  /*
075   * Instance fields.
076   */
077  
078
079  /**
080   * The {@link MavenProject} in effect.
081   *
082   * <p>This field is never {@code null}.</p>
083   *
084   * @see #PackageMojo(MavenProject)
085   */
086  private final MavenProject project;
087
088  /**
089   * Whether to skip execution.
090   */
091  @Parameter(defaultValue = "false")
092  private boolean skip;
093
094  /**
095   * A {@link String} in {@link URI} form pointing to the Helm chart
096   * contents to load.
097   */
098  @Parameter(required = true, defaultValue = "file:${project.basedir}/src/helm/charts/${project.artifactId}/")
099  private String chartContentsUri;
100  
101  @Parameter
102  private AbstractChartLoader<URL> chartLoader;
103
104  @Parameter
105  private AbstractChartWriter chartWriter;
106
107  @Parameter
108  private URI chartTargetUri;
109
110
111  /*
112   * Constructors.
113   */
114
115
116  @Inject
117  public PackageMojo(final MavenProject project) {
118    super();
119    this.project = project;
120  }
121
122
123  /*
124   * Public instance methods.
125   */
126
127  
128  @Override
129  public void execute() throws MojoExecutionException, MojoFailureException {
130    final Log log = this.getLog();
131    assert log != null;
132
133    if (this.getSkip()) {
134      if (log.isDebugEnabled()) {
135        log.debug("Skipping execution by request.");
136      }
137      return;
138    }
139
140    final URI chartContentsUri = this.getChartContentsUri();
141    if (chartContentsUri == null) {
142      throw new IllegalStateException("getChartContentsUri() == null");
143    }
144    URL chartContentsUrl = null;
145    try {
146      chartContentsUrl = chartContentsUri.toURL();
147    } catch (final IOException ioException) {
148      throw new MojoExecutionException(ioException.getMessage(), ioException);
149    }
150    assert chartContentsUrl != null;
151
152    AbstractChartLoader<URL> chartLoader = this.getChartLoader();
153    if (chartLoader == null) {
154      chartLoader = new URLChartLoader();
155    }
156
157    Throwable throwable = null;
158
159    Chart.Builder chart = null;
160    try {
161      chart = chartLoader.load(chartContentsUrl);
162    } catch (final RuntimeException runtimeException) {
163      throwable = runtimeException;
164      throw runtimeException;
165    } catch (final IOException exception) {
166      final MojoExecutionException e = new MojoExecutionException(exception.getMessage(), exception);
167      throwable = e;
168      throw e;
169    } finally {
170      try {
171        chartLoader.close();
172      } catch (final IOException suppressMe) {
173        if (throwable == null) {
174          throw new MojoExecutionException(suppressMe.getMessage(), suppressMe);
175        } else {
176          throwable.addSuppressed(suppressMe);
177        }
178      }
179    }
180    throwable = null;
181
182    final MetadataOrBuilder metadata = chart.getMetadata();
183    if (metadata == null) {
184      throw new IllegalStateException("chart.getMetadata() == null");
185    }
186    final String chartName = metadata.getName();
187    if (chartName == null) {
188      throw new IllegalStateException("metadata.getName() == null");
189    } else if (chartName.isEmpty()) {
190      throw new IllegalStateException("metadata.getName().isEmpty()");
191    }
192
193    AbstractChartWriter chartWriter = this.getChartWriter();
194    if (chartWriter == null) {
195      URI chartTargetUri = this.getChartTargetUri();
196      if (chartTargetUri == null) {
197        final Build build = this.project.getBuild();
198        assert build != null;
199        final String targetDirectory = build.getDirectory();
200        assert targetDirectory != null;
201        final Path targetDirectoryPath = Paths.get(targetDirectory);
202        assert targetDirectoryPath != null;
203        final Path helmChartsDirectoryPath = targetDirectoryPath.resolve("generated-sources/helm/charts");
204        assert helmChartsDirectoryPath != null;
205        assert helmChartsDirectoryPath.isAbsolute();
206        final Path chartDirectoryPath = helmChartsDirectoryPath.resolve(chartName + ".tgz");
207        assert chartDirectoryPath != null;
208        chartTargetUri = chartDirectoryPath.toUri();
209      }
210      assert chartTargetUri != null;
211      if ("file".equals(chartTargetUri.getScheme())) {
212        final String chartTargetUriPath = chartTargetUri.getPath();
213        if (chartTargetUriPath != null) {
214          final Path chartTargetPath = Paths.get(chartTargetUriPath).normalize();
215          assert chartTargetPath != null;
216          final Path parent = chartTargetPath.getParent();
217          if (parent != null) {
218            try {
219              Files.createDirectories(parent);
220            } catch (final IOException ioException) {
221              throw new MojoExecutionException(ioException.getMessage(), ioException);
222            }
223          }
224        }
225      }
226      URL chartTargetURL = null;
227      try {
228        chartTargetURL = chartTargetUri.toURL();
229      } catch (final IOException ioException) {
230        throw new MojoExecutionException(ioException.getMessage(), ioException);
231      }
232      assert chartTargetURL != null;
233      URLConnection connection = null;
234      try {
235        connection = chartTargetURL.openConnection();
236      } catch (final IOException ioException) {
237        throw new MojoExecutionException(ioException.getMessage(), ioException);
238      }      
239      assert connection != null;
240      connection.setDoOutput(true);
241      OutputStream outputStream = null;
242      try {
243        outputStream = connection.getOutputStream();
244      } catch (final UnknownServiceException outputNotSupported) {
245        // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4191800
246        if ("file".equals(chartTargetURL.getProtocol())) {
247          final Path chartTargetPath = Paths.get(chartTargetURL.getPath());
248          assert chartTargetPath != null;
249          try {
250            outputStream = Files.newOutputStream(chartTargetPath);
251          } catch (final IOException ioException) {
252            ioException.addSuppressed(outputNotSupported);
253            throw new MojoExecutionException(ioException.getMessage(), ioException);
254          }
255        } else {
256          throw new MojoExecutionException(outputNotSupported.getMessage(), outputNotSupported);
257        }
258      } catch (final IOException ioException) {
259        throw new MojoExecutionException(ioException.getMessage(), ioException);
260      }
261      try {
262        chartWriter = new TapeArchiveChartWriter(new BufferedOutputStream(new GZIPOutputStream(outputStream)));
263      } catch (final IOException ioException) {
264        throw new MojoExecutionException(ioException.getMessage(), ioException);
265      }
266    }
267    assert chartWriter != null;
268
269    throwable = null;
270    try {
271      chartWriter.write(chart);
272    } catch (final RuntimeException runtimeException) {
273      throwable = runtimeException;
274      throw runtimeException;
275    } catch (final IOException ioException) {
276      final MojoExecutionException e = new MojoExecutionException(ioException.getMessage(), ioException);
277      throwable = e;
278      throw e;
279    } finally {
280      try {
281        chartWriter.close();
282      } catch (final IOException suppressMe) {
283        if (throwable == null) {
284          throw new MojoExecutionException(suppressMe.getMessage(), suppressMe);
285        } else {
286          throwable.addSuppressed(suppressMe);
287        }
288      }
289    }
290    
291  }
292
293  /**
294   * Implements the {@link Disposable} interface by {@linkplain
295   * AbstractChartLoader#close() closing} the {@link
296   * AbstractChartLoader} returned by the {@link #getChartLoader()}
297   * method and {@linkplain AbstractChartWriter#close() closing} the
298   * {@link AbstractChartWriter} returned by the {@link
299   * #getChartWriter()} method.
300   *
301   * <p>Any {@link IOException}s encountered are {@linkplain
302   * Log#error(Throwable) logged}.</p>
303   */
304  @Override
305  public void dispose() {
306    final Log log = this.getLog();
307    assert log != null;
308
309    final Closeable chartLoader = this.getChartLoader();
310    if (chartLoader != null) {
311      try {
312        chartLoader.close();
313      } catch (final IOException logMe) {
314        if (log.isErrorEnabled()) {
315          log.error(logMe);
316        }
317      }
318    }
319
320    final Closeable chartWriter = this.getChartWriter();
321    if (chartWriter != null) {
322      try {
323        chartWriter.close();
324      } catch (final IOException logMe) {
325        if (log.isErrorEnabled()) {
326          log.error(logMe);
327        }
328      }
329    }
330  }
331
332  /**
333   * Returns {@code true} if this {@link PackageMojo} should not
334   * execute.
335   *
336   * @return {@code true} if this {@link PackageMojo} should not
337   * execute; {@code false} otherwise
338   *
339   * @see #setSkip(boolean)
340   */
341  public boolean getSkip() {
342    return this.skip;
343  }
344
345  /**
346   * Controls whether this {@link PackageMojo} should execute.
347   *
348   * @param skip if {@code true}, this {@link PackageMojo} will not
349   * execute
350   *
351   * @see #getSkip()
352   */
353  public void setSkip(final boolean skip) {
354    this.skip = skip;
355  }
356
357  public AbstractChartLoader<URL> getChartLoader() {
358    return this.chartLoader;
359  }
360
361  public void setChartLoader(final AbstractChartLoader<URL> chartLoader) {
362    this.chartLoader = chartLoader;
363  }
364
365  public AbstractChartWriter getChartWriter() {
366    return this.chartWriter;
367  }
368
369  public void setChartWriter(final AbstractChartWriter chartWriter) {
370    this.chartWriter = chartWriter;
371  }
372
373  public URI getChartContentsUri() {
374    final URI returnValue;
375    if (this.chartContentsUri == null) {
376      returnValue = null;
377    } else {
378      URI temp = null;
379      try {
380        temp = new URI(this.chartContentsUri);
381      } catch (final URISyntaxException uriSyntaxException) {
382        try {
383          temp = new URI(this.chartContentsUri.replace('\\', '/'));
384        } catch (final URISyntaxException backslashReplacementFailed) {
385          throw new IllegalStateException(backslashReplacementFailed.getMessage(), backslashReplacementFailed);
386        }
387      } finally {
388        returnValue = temp;
389      }
390    }
391    return returnValue;
392  }
393
394  public void setChartContentsUri(final URI chartContentsUri) {
395    if (chartContentsUri == null) {
396      this.chartContentsUri = null;
397    } else {
398      this.chartContentsUri = chartContentsUri.toString();
399    }
400  }
401
402  public URI getChartTargetUri() {
403    return this.chartTargetUri;
404  }
405
406  public void setChartTargetUri(final URI chartTargetUri) {
407    this.chartTargetUri = chartTargetUri;
408  }
409
410  
411}