View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2018 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.devops.conga.tooling.maven.plugin.util;
21  
22  import static org.apache.maven.artifact.Artifact.SCOPE_COMPILE;
23  
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Objects;
30  import java.util.Set;
31  
32  import org.apache.commons.lang3.StringUtils;
33  import org.apache.maven.project.MavenProject;
34  import org.eclipse.aether.RepositorySystem;
35  import org.eclipse.aether.RepositorySystemSession;
36  import org.eclipse.aether.artifact.Artifact;
37  import org.eclipse.aether.artifact.ArtifactType;
38  import org.eclipse.aether.artifact.DefaultArtifact;
39  import org.eclipse.aether.graph.Dependency;
40  import org.eclipse.aether.repository.RemoteRepository;
41  import org.eclipse.aether.resolution.ArtifactDescriptorException;
42  import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
43  import org.eclipse.aether.resolution.ArtifactDescriptorResult;
44  import org.eclipse.aether.resolution.ArtifactRequest;
45  import org.eclipse.aether.resolution.ArtifactResolutionException;
46  import org.eclipse.aether.resolution.ArtifactResult;
47  
48  import io.wcm.devops.conga.generator.spi.context.PluginContextOptions;
49  import io.wcm.devops.conga.model.environment.Environment;
50  import io.wcm.devops.conga.tooling.maven.plugin.urlfile.MavenUrlFilePlugin;
51  
52  /**
53   * Helper for resolving maven artifacts.
54   */
55  public class MavenArtifactHelper {
56  
57    private final MavenProject project;
58    private final RepositorySystem repoSystem;
59    private final RepositorySystemSession repoSession;
60    private final List<RemoteRepository> remoteRepos;
61    private final Map<String, String> artifactTypeMappings;
62    private final List<String> environmentDependencyUrls;
63    private final PluginContextOptions pluginContextOptions;
64  
65    /**
66     * @param environment CONGA environment
67     * @param pluginContextOptions Plugin context options
68     */
69    public MavenArtifactHelper(Environment environment, PluginContextOptions pluginContextOptions) {
70      MavenContext mavenContext = (MavenContext)pluginContextOptions.getContainerContext();
71      this.project = mavenContext.getProject();
72      this.repoSystem = mavenContext.getRepoSystem();
73      this.repoSession = mavenContext.getRepoSession();
74      this.remoteRepos = mavenContext.getRemoteRepos();
75      this.artifactTypeMappings = mavenContext.getArtifactTypeMappings();
76      this.environmentDependencyUrls = environment != null ? environment.getDependencies() : List.of();
77      this.pluginContextOptions = pluginContextOptions;
78    }
79  
80    /**
81     * Get Maven artifact for given artifact coordinates.
82     * @param artifactCoords Artifact coordinates in either Maven-style or Pax URL-style.
83     * @return Maven artifact
84     * @throws IOException If artifact resolution was not successful
85     */
86    public Artifact resolveArtifact(String artifactCoords) throws IOException {
87      Artifact artifact;
88      if (StringUtils.contains(artifactCoords, "/")) {
89        artifact = getArtifactFromMavenCoordinatesPaxUrlStyle(artifactCoords);
90      }
91      else {
92        artifact = getArtifactFromMavenCoordinates(artifactCoords);
93      }
94      return resolveArtifact(artifact);
95    }
96  
97    /**
98     * Get Maven artifact for given artifact coordinates.
99     * @param groupId Group Id
100    * @param artifactId Artifact Id
101    * @param type Type
102    * @param classifier Classifier
103    * @param version Version
104    * @return Artifact
105    * @throws IOException If dependency resolution fails
106    */
107   @SuppressWarnings("PMD.UseObjectForClearerAPI")
108   public Artifact resolveArtifact(String groupId, String artifactId, String type, String classifier, String version) throws IOException {
109     Artifact artifact = createArtifact(groupId, artifactId, type, classifier, version);
110     return resolveArtifact(artifact);
111   }
112 
113   /**
114    * Get transitive compile dependencies of given artifact.
115    * @param artifact Maven artifact
116    * @return List of artifact dependencies
117    * @throws IOException If artifact resolution was not successful
118    */
119   public List<Artifact> getTransitiveDependencies(Artifact artifact) throws IOException {
120     List<Artifact> dependencies = new ArrayList<>();
121     ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest();
122     descriptorRequest.setArtifact(artifact);
123     descriptorRequest.setRepositories(remoteRepos);
124     try {
125       ArtifactDescriptorResult result = repoSystem.readArtifactDescriptor(repoSession, descriptorRequest);
126       for (Dependency dependency : result.getDependencies()) {
127         if (StringUtils.equals(dependency.getScope(), SCOPE_COMPILE)) {
128           Artifact resolvedArtifact = resolveArtifact(dependency.getArtifact());
129           dependencies.add(resolvedArtifact);
130         }
131       }
132     }
133     catch (ArtifactDescriptorException ex) {
134       throw new IOException("Unable to get artifact descriptor for: '" + artifact + "': " + ex.getMessage(), ex);
135     }
136     return dependencies;
137   }
138 
139   /**
140    * Transform a list of dependency urls to a list of references maven artifacts including their transitive
141    * dependencies.
142    * @param dependencyUrls Dependency URLs
143    * @return List of artifacts
144    * @throws IOException If dependency resolution fails
145    */
146   public List<Artifact> dependencyUrlsToArtifactsWithTransitiveDependencies(Collection<String> dependencyUrls) throws IOException {
147     List<Artifact> artifacts = new ArrayList<>();
148     for (String dependencyUrl : environmentDependencyUrls) {
149       String resolvedDependencyUrl = ClassLoaderUtil.resolveDependencyUrl(dependencyUrl, pluginContextOptions);
150       if (!StringUtils.startsWith(resolvedDependencyUrl, MavenUrlFilePlugin.PREFIX)) {
151         continue;
152       }
153 
154       String mavenCoords = MavenUrlFilePlugin.getMavenCoords(resolvedDependencyUrl);
155       Artifact artifact = resolveArtifact(mavenCoords);
156       artifacts.add(artifact);
157       artifacts.addAll(getTransitiveDependencies(artifact));
158     }
159     return artifacts;
160   }
161 
162   /**
163    * Parse coordinates following definition from https://maven.apache.org/pom.html#Maven_Coordinates
164    * @param artifactCoords Artifact coordinates
165    * @return Artifact object
166    */
167   @SuppressWarnings("PMD.PreserveStackTrace")
168   private Artifact getArtifactFromMavenCoordinates(String artifactCoords) throws IOException {
169     try {
170       Artifact artifact = new DefaultArtifact(artifactCoords);
171       return createArtifact(artifact.getGroupId(), artifact.getArtifactId(), artifact.getExtension(), artifact.getClassifier(), artifact.getVersion());
172     }
173     catch (IllegalArgumentException ex) {
174       throw new IOException(ex.getMessage());
175     }
176   }
177 
178   /**
179    * Parse coordinates in slingstart/Pax URL style following definition from https://ops4j1.jira.com/wiki/x/CoA6
180    * @param artifactCoords Artifact coordinates
181    * @return Artifact object
182    */
183   private Artifact getArtifactFromMavenCoordinatesPaxUrlStyle(String artifactCoords) throws IOException {
184     String[] parts = StringUtils.splitPreserveAllTokens(artifactCoords, "/");
185 
186     String version = null;
187     String packaging = null;
188     String classifier = null;
189 
190     switch (parts.length) {
191       case 2:
192         // groupId/artifactId
193         break;
194 
195       case 3:
196         // groupId/artifactId/version
197         version = StringUtils.defaultIfBlank(parts[2], null);
198         break;
199 
200       case 4:
201         // groupId/artifactId/version/type
202         packaging = StringUtils.defaultIfBlank(parts[3], null);
203         version = StringUtils.defaultIfBlank(parts[2], null);
204         break;
205 
206       case 5:
207         // groupId/artifactId/version/type/classifier
208         packaging = StringUtils.defaultIfBlank(parts[3], null);
209         classifier = StringUtils.defaultIfBlank(parts[4], null);
210         version = StringUtils.defaultIfBlank(parts[2], null);
211         break;
212 
213       default:
214         throw new IOException("Invalid artifact: " + artifactCoords);
215     }
216 
217     String groupId = StringUtils.defaultIfBlank(parts[0], null);
218     String artifactId = StringUtils.defaultIfBlank(parts[1], null);
219 
220     return createArtifact(groupId, artifactId, packaging, classifier, version);
221   }
222 
223   private Artifact createArtifact(String groupId, String artifactId, String type, String classifier, String version) throws IOException {
224 
225     String artifactTypeString = Objects.toString(type, "jar");
226     String artifactExtension = artifactTypeString;
227 
228     ArtifactType artifactType = repoSession.getArtifactTypeRegistry().get(artifactExtension);
229     if (artifactType != null) {
230       artifactExtension = artifactType.getExtension();
231     }
232 
233     // apply custom mapping from artifact type to extension if defined in plugin config
234     if (artifactTypeMappings != null && artifactTypeMappings.containsKey(artifactExtension)) {
235       artifactExtension = artifactTypeMappings.get(artifactExtension);
236     }
237 
238     String artifactVersion = version;
239     if (artifactVersion == null) {
240       artifactVersion = resolveArtifactVersion(groupId, artifactId, artifactTypeString, classifier);
241     }
242 
243     if (StringUtils.isBlank(groupId) || StringUtils.isBlank(artifactId) || StringUtils.isBlank(artifactVersion)) {
244       throw new IOException("Invalid Maven artifact reference: "
245           + "artifactId=" + artifactId + ", "
246           + "groupId=" + groupId + ", "
247           + "version=" + artifactVersion + ", "
248           + "extension=" + artifactExtension + ", "
249           + "classifier=" + classifier + ","
250           + "type=" + artifactType);
251     }
252 
253     return new DefaultArtifact(groupId, artifactId, classifier, artifactExtension, artifactVersion, artifactType);
254   }
255 
256   private Artifact resolveArtifact(Artifact artifact) throws IOException {
257     ArtifactRequest artifactRequest = new ArtifactRequest();
258     artifactRequest.setArtifact(artifact);
259     artifactRequest.setRepositories(remoteRepos);
260     try {
261       ArtifactResult result = repoSystem.resolveArtifact(repoSession, artifactRequest);
262       return result.getArtifact();
263     }
264     catch (final ArtifactResolutionException ex) {
265       throw new IOException("Unable to get artifact for '" + artifact + "': " + ex.getMessage(), ex);
266     }
267   }
268 
269   private String resolveArtifactVersion(String groupId, String artifactId, String type, String classifier) throws IOException {
270     String version = findVersionInMavenProject(groupId, artifactId, type, classifier);
271     if (version == null) {
272       version = findVersionInEnvironmentDependencies(groupId, artifactId, type, classifier);
273     }
274     return version;
275   }
276 
277   private String findVersionInMavenProject(String groupId, String artifactId, String type, String classifier) {
278     Set<org.apache.maven.artifact.Artifact> dependencies = project.getArtifacts();
279     if (dependencies != null) {
280       for (org.apache.maven.artifact.Artifact dependency : dependencies) {
281         if (artifactEquals(dependency, artifactId, groupId, type, classifier)) {
282           return dependency.getVersion();
283         }
284       }
285     }
286     return null;
287   }
288 
289   private boolean artifactEquals(org.apache.maven.artifact.Artifact dependency, String artifactId, String groupId, String type, String classifier) {
290     return StringUtils.equals(dependency.getGroupId(), groupId)
291         && StringUtils.equals(dependency.getArtifactId(), artifactId)
292         && StringUtils.equals(StringUtils.defaultString(dependency.getClassifier()), StringUtils.defaultString(classifier))
293         && StringUtils.equals(dependency.getType(), type);
294   }
295 
296   private String findVersionInEnvironmentDependencies(String groupId, String artifactId, String packaging, String classifier) throws IOException {
297     List<Artifact> dependencies = dependencyUrlsToArtifactsWithTransitiveDependencies(environmentDependencyUrls);
298 
299     for (Artifact dependency : dependencies) {
300       if (artifactEquals(dependency, groupId, artifactId, packaging, classifier)) {
301         return dependency.getVersion();
302       }
303     }
304 
305     return null;
306   }
307 
308   private boolean artifactEquals(Artifact dependency, String groupId, String artifactId, String type, String classifier) {
309     return StringUtils.equals(dependency.getGroupId(), groupId)
310         && StringUtils.equals(dependency.getArtifactId(), artifactId)
311         && StringUtils.equals(StringUtils.defaultString(dependency.getClassifier()), StringUtils.defaultString(classifier))
312         && StringUtils.equals(dependency.getExtension(), type);
313   }
314 
315 }