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}