001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2017 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.BufferedInputStream;
020import java.io.File;
021import java.io.IOException;
022
023import java.nio.file.Files;
024
025import java.util.Collections;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Objects;
029
030import java.util.zip.GZIPInputStream;
031import java.util.zip.ZipInputStream;
032
033import hapi.chart.ChartOuterClass.Chart;
034
035import org.eclipse.aether.RepositorySystemSession;
036import org.eclipse.aether.RepositorySystem;
037
038import org.eclipse.aether.artifact.Artifact;
039import org.eclipse.aether.artifact.DefaultArtifact;
040
041import org.eclipse.aether.repository.RemoteRepository;
042
043import org.eclipse.aether.resolution.ArtifactRequest;
044import org.eclipse.aether.resolution.ArtifactResolutionException;
045import org.eclipse.aether.resolution.ArtifactResult;
046
047import org.kamranzafar.jtar.TarInputStream;
048
049import org.microbean.helm.chart.AbstractChartLoader; // for javadoc only
050import org.microbean.helm.chart.TapeArchiveChartLoader;
051import org.microbean.helm.chart.ZipInputStreamChartLoader;
052
053import org.microbean.helm.chart.resolver.AbstractChartResolver;
054import org.microbean.helm.chart.resolver.ChartResolverException;
055
056/**
057 * An {@link AbstractChartResolver} capable of resolving Helm charts
058 * from Maven repositories.
059 *
060 * @author <a href="https://about.me/lairdnelson"
061 * target="_parent">Laird Nelson</a>
062 *
063 * @see #resolve(String, String)
064 */
065public class MavenRepositoryChartResolver extends AbstractChartResolver {
066
067
068  /*
069   * Instance fields.
070   */
071
072
073  /**
074   * The {@link RepositorySystem} responsible for performing the
075   * actual resolution.
076   *
077   * <p>This field is never {@code null}.</p>
078   *
079   * @see #MavenRepositoryChartResolver(RepositorySystem,
080   * RepositorySystemSession, List)
081   *
082   * @see RepositorySystem
083   */
084  private final RepositorySystem repositorySystem;
085
086  /**
087   * The {@link RepositorySystemSession} governing artifact resolution.
088   *
089   * <p>This field is never {@code null}.</p>
090   *
091   * @see #MavenRepositoryChartResolver(RepositorySystem,
092   * RepositorySystemSession, List)
093   *
094   * @see RepositorySystemSession
095   */
096  private final RepositorySystemSession session;
097
098  /**
099   * A {@link List} of {@link RemoteRepository} instances from which
100   * resolution will be attempted.
101   *
102   * <p>This field may be {@code null}.</p>
103   *
104   * @see #MavenRepositoryChartResolver(RepositorySystem,
105   * RepositorySystemSession, List)
106   *
107   * @see RemoteRepository
108   */
109  private final List<RemoteRepository> remoteRepositories;
110
111
112  /*
113   * Constructors.
114   */
115
116
117  /**
118   * Creates a new {@link MavenRepositoryChartResolver}.
119   *
120   * @param repositorySystem the {@link RepositorySystem} responsible
121   * for performing the actual resolution; must not be {@code null}
122   *
123   * @param repositorySystemSession the {@link
124   * RepositorySystemSession} governing artifact resolution; must not
125   * be {@code null}
126   *
127   * @param remoteRepositories a {@link List} of {@link
128   * RemoteRepository} instances from which resolution will be
129   * attempted; may be {@code null}
130   *
131   * @exception NullPointerException if {@code repositorySystem} or
132   * {@code session} is {@code null}
133   *
134   * @see #getRepositorySystem()
135   *
136   * @see #getSession()
137   *
138   * @see #getRemoteRepositories()
139   */
140  public MavenRepositoryChartResolver(final RepositorySystem repositorySystem,
141                                      final RepositorySystemSession session,
142                                      final List<RemoteRepository> remoteRepositories) {
143    super();
144    this.repositorySystem = Objects.requireNonNull(repositorySystem);
145    this.session = Objects.requireNonNull(session);
146    this.remoteRepositories = remoteRepositories;
147  }
148
149
150  /*
151   * Instance methods.
152   */
153
154
155  /**
156   * Returns the {@link RepositorySystem} responsible for performing the
157   * actual resolution.
158   *
159   * <p>This method never returns {@code null}.</p>
160   *
161   * <p>Overrides of this method must not return {@code null}.</p>
162   *
163   * @return the {@link RepositorySystem} responsible for performing the
164   * actual resolution; never {@code null}
165   *
166   * @see #MavenRepositoryChartResolver(RepositorySystem,
167   * RepositorySystemSession, List)
168   */
169  public RepositorySystem getRepositorySystem() {
170    return this.repositorySystem;
171  }
172
173  /**
174   * Returns the {@link RepositorySystemSession} governing resolution.
175   *
176   * <p>This method never returns {@code null}.</p>
177   *
178   * <p>Overrides of this method must not return {@code null}.</p>
179   *
180   * @return the {@link RepositorySystemSession} governing resolution;
181   * never {@code null}
182   *
183   * @see #MavenRepositoryChartResolver(RepositorySystem,
184   * RepositorySystemSession, List)
185   */
186  public RepositorySystemSession getSession() {
187    return this.session;
188  }
189
190  /**
191   * Returns the {@link List} of {@link RemoteRepository} instances
192   * from which resolution will be attempted.
193   *
194   * <p>This method may return {@code null}.</p>
195   *
196   * <p>Overrides of this method are permitted to return {@code
197   * null}.</p>
198   *
199   * @return the {@link List} of {@link RemoteRepository} instances
200   * from which resolution will be attempted, or {@code null}
201   *
202   * @see #MavenRepositoryChartResolver(RepositorySystem,
203   * RepositorySystemSession, List)
204   */
205  public List<RemoteRepository> getRemoteRepositories() {
206    return this.remoteRepositories;
207  }
208
209  /**
210   * Creates and returns a {@link
211   * hapi.chart.ChartOuterClass.Chart.Builder} representing a Helm
212   * chart resolvable at the given Maven repository coordinates.
213   *
214   * @param coordinatesWithoutVersion a {@link String} of one of the
215   * following forms: {@code groupId:artifactId}, {@code
216   * groupId:artifactId:packaging}, or {@code
217   * groupId:artifactId:packaging:classifier}; must not be {@code
218   * null}
219   *
220   * @param chartVersion the version of the Helm chart artifact to
221   * resolve; may be {@code null} in which case {@code LATEST} will be
222   * used instead
223   *
224   * @return a non-{@code null} {@link
225   * hapi.chart.ChartOuterClass.Chart.Builder}
226   *
227   * @exception NullPointerException if {@code coordinatesWithoutVersion} is {@code null}
228   *
229   * @exception ChartResolverException if {@code
230   * coordinatesWithoutVersion} is malformed, if either the {@link
231   * #getRepositorySystem()} or {@link #getSession()} method returns
232   * {@code null}, if an {@link ArtifactResolutionException} was
233   * encountered during artifact resolution, or if the {@link
234   * #loadChart(File, String)} method throws a {@link
235   * ChartResolverException}
236   *
237   * @see #resolve(Artifact)
238   *
239   * @see #loadChart(File, String)
240   */
241  @Override
242  public final Chart.Builder resolve(final String coordinatesWithoutVersion, String chartVersion) throws ChartResolverException {
243    Objects.requireNonNull(coordinatesWithoutVersion);
244    if (chartVersion == null) {
245      chartVersion = "LATEST"; // TODO: not sure if this will work
246    }
247    
248    final String[] parts = coordinatesWithoutVersion.split(":");
249    assert parts != null;
250    if (parts.length < 2 || parts.length > 4) {
251      throw new ChartResolverException(new IllegalArgumentException("coordinatesWithoutVersion: " + coordinatesWithoutVersion));
252    }
253    final String groupId = parts[0];
254    final String artifactId = parts[1];
255    final String packaging;
256    final String classifier;
257    if (parts.length >= 3) {
258      packaging = parts[2];
259      if (parts.length == 4) {
260        classifier = parts[3];
261      } else {
262        classifier = null;
263      }
264    } else {
265      packaging = "tgz";
266      classifier = null;
267    }
268
269    System.out.println("*** groupId: " + groupId);
270    System.out.println("*** artifactId: " + artifactId);
271    System.out.println("*** packaging: " + packaging);
272    System.out.println("*** classifier: " + classifier);
273
274    final Artifact chart = new DefaultArtifact(groupId, artifactId, classifier, packaging, chartVersion);
275    return this.resolve(chart);
276  }
277
278  /**
279   * Creates and returns a {@link
280   * hapi.chart.ChartOuterClass.Chart.Builder} representing a Helm
281   * chart resolvable at the Maven repository coordinates represented
282   * by the supplied {@link Artifact}.
283   *
284   * <p>This method never returns {@code null}.</p>
285   *
286   * <p>Overrides of this method must not return {@code null}.</p>
287   *
288   * @param chart the {@link Artifact} representing the Helm chart to
289   * resolve; must not be {@code null}
290   *
291   * @return a non-{@code null} {@link
292   * hapi.chart.ChartOuterClass.Chart.Builder}
293   *
294   * @exception NullPointerException if {@code chart} is {@code null}
295   *
296   * @exception ChartResolverException if either the {@link
297   * #getRepositorySystem()} or {@link #getSession()} method returns
298   * {@code null}, if an {@link ArtifactResolutionException} was
299   * encountered during artifact resolution, or if the {@link
300   * #loadChart(File, String)} method throws a {@link
301   * ChartResolverException}
302   *
303   * @see #loadChart(File, String)
304   */
305  public Chart.Builder resolve(final Artifact chart) throws ChartResolverException {
306    Objects.requireNonNull(chart);
307
308    final RepositorySystem repositorySystem = this.getRepositorySystem();
309    if (repositorySystem == null) {
310      throw new ChartResolverException(new IllegalStateException("getRepositorySystem() == null"));
311    }
312
313    final RepositorySystemSession session = this.getSession();
314    if (session == null) {
315      throw new ChartResolverException(new IllegalStateException("getSession() == null"));
316    }
317    
318    List<RemoteRepository> remoteRepositories = this.getRemoteRepositories();
319    if (remoteRepositories == null) {
320      remoteRepositories = Collections.emptyList();
321    }
322    
323    final ArtifactRequest request = new ArtifactRequest(chart, remoteRepositories, null);
324
325    ArtifactResult result = null;
326    try {
327      result = repositorySystem.resolveArtifact(session, request);
328    } catch (final ArtifactResolutionException artifactResolutionException) {
329      throw new ChartResolverException(artifactResolutionException);
330    }
331    
332    final Artifact resolvedChart;
333    if (result.isResolved()) {
334      resolvedChart = result.getArtifact();
335    } else {
336      resolvedChart = null;
337    }
338    
339    if (resolvedChart == null) {
340      final List<? extends Exception> exceptions = result.getExceptions();
341      if (exceptions == null || exceptions.isEmpty()) {
342        throw new ChartResolverException();
343      } else if (exceptions.size() == 1) {
344        throw new ChartResolverException(exceptions.get(0));
345      } else {
346        final Iterator<? extends Exception> iterator = exceptions.iterator();
347        assert iterator != null;
348        assert iterator.hasNext();
349        final Exception root = iterator.next();
350        assert root != null;
351        final ChartResolverException throwMe = new ChartResolverException(root);
352        assert iterator.hasNext();
353        while (iterator.hasNext()) {
354          throwMe.addSuppressed(iterator.next());
355        }
356        throw throwMe;
357      }
358    }
359    assert resolvedChart != null;
360    
361    final File chartFile = resolvedChart.getFile();
362    assert chartFile != null;
363
364    final Chart.Builder returnValue = this.loadChart(chartFile, chart.getExtension());
365    return returnValue;
366  }
367
368  /**
369   * Returns a {@link hapi.chart.ChartOuterClass.Chart.Builder}
370   * representing the Helm chart contained by the supplied {@linkplain
371   * File#isFile() regular} {@link File}.
372   *
373   * <p>This method never returns {@code null}.</p>
374   *
375   * <p>Overrides of this method must not return {@code null}.</p>
376   *
377   * <p>This implementation will return a {@link
378   * hapi.chart.ChartOuterClass.Chart.Builder} provided that the
379   * supplied {@link File} is either a GZIP-encoded tape archive or a
380   * ZIP archive.  Overrides should feel free to expand these
381   * capabilities.</p>
382   *
383   * @param chartFile the {@link File} containing a Helm chart; must
384   * not be {@code null}
385   *
386   * @param packaging the kind of archive or other packaging mechanism
387   * that the supplied {@code chartFile} value is; must not be {@code
388   * null}; examples include {@code tgz}, {@code tar.gz}, {@code zip}
389   * and the like; overrides of this method may use this parameter to
390   * help in determining how to read the supplied {@code chartFile}
391   *
392   * @return a non-{@code null} {@link
393   * hapi.chart.ChartOuterClass.Chart.Builder}, usually but not
394   * necessarily as produced by an {@link AbstractChartLoader}
395   * implementation
396   *
397   * @exception NullPointerException if {@code chartFile} or {@code
398   * packaging} is {@code null}
399   *
400   * @exception ChartResolverException if the chart could not be
401   * loaded for any reason
402   *
403   * @see #resolve(String, String)
404   */
405  protected Chart.Builder loadChart(final File chartFile, final String packaging) throws ChartResolverException {
406    Objects.requireNonNull(chartFile);
407    Objects.requireNonNull(packaging);
408    Chart.Builder returnValue = null;
409    if ("tgz".equalsIgnoreCase(packaging) || "tar.gz".equalsIgnoreCase(packaging) || "helm.tar.gz".equalsIgnoreCase(packaging)) {
410      try (final TapeArchiveChartLoader loader = new TapeArchiveChartLoader()) {
411        returnValue = loader.load(new TarInputStream(new GZIPInputStream(new BufferedInputStream(Files.newInputStream(chartFile.toPath())))));
412      } catch (final IOException exception) {
413        throw new ChartResolverException(exception.getMessage(), exception);
414      }
415    } else if ("jar".equalsIgnoreCase(packaging) || "zip".equalsIgnoreCase(packaging)) {
416      try (final ZipInputStreamChartLoader loader = new ZipInputStreamChartLoader()) {
417        returnValue = loader.load(new ZipInputStream(new BufferedInputStream(Files.newInputStream(chartFile.toPath()))));
418      } catch (final IOException exception) {
419        throw new ChartResolverException(exception.getMessage(), exception);
420      }
421    } else {
422      throw new ChartResolverException("Cannot load chart: " + chartFile + "; unhandled packaging: " + packaging);
423    }
424    return returnValue;
425  }
426  
427}