View Javadoc

1   package com.atlassian.maven.plugins.licenses;
2   
3   import java.io.File;
4   import java.net.MalformedURLException;
5   import java.util.ArrayList;
6   import java.util.Arrays;
7   import java.util.Iterator;
8   import java.util.List;
9   import java.util.Set;
10  import java.util.StringTokenizer;
11  import java.util.TreeSet;
12  import java.util.jar.JarFile;
13  
14  import org.apache.maven.artifact.Artifact;
15  import org.apache.maven.artifact.factory.ArtifactFactory;
16  import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
17  import org.apache.maven.artifact.repository.ArtifactRepository;
18  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
19  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
20  import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
21  import org.apache.maven.artifact.resolver.ArtifactResolver;
22  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
23  import org.apache.maven.model.License;
24  import org.apache.maven.plugin.AbstractMojo;
25  import org.apache.maven.plugin.MojoExecutionException;
26  import org.apache.maven.project.MavenProject;
27  import org.apache.maven.project.MavenProjectBuilder;
28  import org.apache.maven.project.ProjectBuildingException;
29  import org.apache.oro.text.regex.MalformedPatternException;
30  
31  /**
32   * Pre-resolves all transitive dependencies. Provides methods for iterating
33   * through dependency projects, searching for the license in the JAR.
34   * 
35   * @requiresDependencyResolution
36   * @author Sherali Karimov
37   */
38  public abstract class AbstractLicensesMojo extends AbstractMojo
39  {
40      /**
41       * Various locations where people historically have placed their license
42       * agreement text inside the JAR files
43       */
44      protected static final List LICENSE_PATH_OPTIONS = Arrays.asList(new String[]
45      {
46              "LICENSE",
47              "LICENSE.txt", 
48              "META-INF/LICENSE", 
49              "META-INF/LICENSE.txt", 
50              "license", 
51              "license.txt",
52              "META-INF/license", 
53              "META-INF/license.txt", 
54      });
55  
56      public static final String LICENSE_TYPE = "license";
57  
58      /**
59       * @component
60       */
61      protected ArtifactResolver resolver;
62  
63      /**
64       * @parameter expression="${localRepository}"
65       */
66      protected ArtifactRepository localRepository;
67  
68      /**
69       * @parameter expression="${project.remoteArtifactRepositories}"
70       */
71      protected List remoteRepositories;
72  
73      /**
74       * @component
75       */
76      protected ArtifactFactory artifactFactory;
77  
78      /**
79       * @component role="org.apache.maven.artifact.metadata.ArtifactMetadataSource"
80       *            hint="maven"
81       */
82      protected ArtifactMetadataSource artifactMetadataSource;
83  
84      /**
85       * @component
86       */
87      protected MavenProjectBuilder mavenProjectBuilder;
88  
89      /**
90       * @parameter expression="${project}"
91       * @required
92       * @readonly
93       */
94      private MavenProject project;
95  
96      /**
97       * List of scopes of dependencies to be excluded
98       * 
99       * @parameter
100      */
101     protected List excludedScopeList;
102 
103     /**
104      * Comma-separated list of scopes of dependencies to be excluded. This is a
105      * command-line alternative to the excludedScope parameter, since List
106      * parameters are not currently compatible with CLI specification.
107      * 
108      * @parameter expression="${licenses.excludedScopes}"
109      */
110     protected String excludedScopes;
111 
112     /**
113      * List of dependencies to be excluded. Each item is a groupId:artifactId
114      * combination. Wildcard expressions: groupId:* and *:artifactId are also
115      * supported.
116      * 
117      * @parameter
118      */
119     protected List excludedLibraryList;
120 
121     /**
122      * Comma-separated list of dependencies to be excluded. Each item is a
123      * groupId:artifactId combination. Wildcard expressions: groupId:* and
124      * *:artifactId are also supported. This is a command-line alternative to
125      * the excludedScope parameter, since List parameters are not currently
126      * compatible with CLI specification.
127      * 
128      * @parameter expression="${licenses.excludedLibraries}"
129      */
130     protected String excludedLibraries;
131 
132     /**
133      * The projects in the reactor for aggregation report.
134      * 
135      * @parameter expression="${reactorProjects}"
136      * @readonly
137      */
138     private List reactorProjects;
139 
140     /**
141      * Whether to work at the root, or work on each module independently
142      * 
143      * @parameter expression="${licenses.aggregate}" default-value="true"
144      */
145     protected boolean aggregate;
146 
147     protected void visitDependencyProjects(ProjectVisitor visitor) throws MojoExecutionException
148     {
149         if (aggregate && !project.isExecutionRoot())
150         {
151             return;
152         }
153 
154         final Set dependencies = resolveDependencies();
155 
156         visitor.onInit(dependencies.size());
157 
158         for (Iterator iterator = dependencies.iterator(); iterator.hasNext();)
159         {
160             final Artifact depArtifact = (Artifact) iterator.next();
161             try
162             {
163                 // for each dependency we need to build a project in order to
164                 // retrieve its licensing information
165                 MavenProject depProject = mavenProjectBuilder.buildFromRepository(depArtifact, remoteRepositories,
166                         localRepository);
167                 visitor.visit(depProject);
168             }
169             catch (ProjectBuildingException e)
170             {
171                 throw new MojoExecutionException("Failed to build a project for artifact: " + depArtifact, e);
172             }
173         }
174         visitor.onComplete();
175     }
176 
177     /**
178      * Attempts to extract the license location for the project. If that fails -
179      * performs a search for other potential license locations inside the JAR as
180      * defined in {@link #LICENSE_PATH_OPTIONS}
181      * 
182      * @param license -
183      *            license object from the maven project. If this is null, this
184      *            method will search the artifact location
185      * @param artifact -
186      *            artifact for which the license is being looked up
187      * @return a valid URL to the license file or null if no valid JAR or entry
188      *         is found
189      */
190     protected String createValidLicenseUrl(License license, Artifact artifact)
191     {
192         String urlString = findLicenseInRepository(artifact);
193         if (urlString != null)
194             return urlString;
195 
196         if (license != null)
197         {
198             urlString = license.getUrl();
199         }
200 
201         if (urlString == null || urlString.startsWith("/") || urlString.startsWith("\\"))
202         {
203             // this is a path inside the JAr file - we need to turn it into a valid URL
204             return findLicenseInJar(artifact, urlString);
205         }
206 
207         // otherwise it is a remote URL
208         return urlString;
209     }
210 
211     /**
212      * Returns a download URL of the corresponding artifact with classifier
213      * 'license' for the given artifact or null if license isn't found in maven
214      * repository.
215      * 
216      * @param artifact
217      * @return a valid URL to the license file or null if no license isn't in
218      *         maven repository
219      */
220     protected String findLicenseInRepository(Artifact artifact)
221     {
222         Artifact licenseArtifact = artifactFactory.createArtifact(artifact.getGroupId(), artifact.getArtifactId(),
223                 artifact.getVersion(), null, LICENSE_TYPE);
224         try
225         {
226             resolver.resolve(licenseArtifact, remoteRepositories, localRepository);
227             if (licenseArtifact.getFile() != null)
228             {
229                 try
230                 {
231                     return licenseArtifact.getFile().toURL().toString();
232                 }
233                 catch (MalformedURLException e)
234                 {
235                     getLog().warn("Failed to convert " + licenseArtifact.getFile() + " to a URL", e);
236                     return "file:" + licenseArtifact.getFile().getAbsolutePath();
237                 }
238             }
239             else
240                 return licenseArtifact.getDownloadUrl();
241         }
242         catch (ArtifactResolutionException e)
243         {
244             getLog().info("Failed to resolve license artifact " + licenseArtifact, e);
245             return null;
246         }
247         catch (ArtifactNotFoundException e)
248         {
249             if (getLog().isDebugEnabled())
250                 getLog().debug("License artifact " + licenseArtifact + " is not found", e);
251             return null;
252         }
253     }
254 
255     /**
256      * Attempts to find the JAR file for this artifact and then the given file
257      * path in the JAR. If the entry does not exist - will try to search for
258      * other potential license locations inside the JAR as defined in
259      * {@link #LICENSE_PATH_OPTIONS}
260      * 
261      * @param artifact -
262      *            will be used to lookup the JAR file for this artifact
263      * @param filePath -
264      *            path to the license inside the JAR
265      * @return a valid URL to the license file or null if no valid JAR or entry
266      *         is found
267      */
268     private String findLicenseInJar(Artifact artifact, String filePath)
269     {
270         final String jarPath = "jar:" + localRepository.getUrl() + File.separator + localRepository.pathOf(artifact);
271 
272         if (filePath != null && filePath.length() != 0)
273         {
274             // check the given URL if it is valid
275             // make sure we do not much up the path as JAR entries are sensitive
276             // to having "//" in them
277             if (filePath.startsWith("/") || filePath.startsWith("\\"))
278             {
279                 filePath = "META-INF" + filePath;
280             }
281             else
282             {
283                 filePath = "META-INF/" + filePath;
284             }
285 
286             // validate
287             String url = toUrl(jarPath, filePath);
288             if (url != null)
289                 return url;
290         }
291 
292         for (Iterator iter = LICENSE_PATH_OPTIONS.iterator(); iter.hasNext();)
293         {
294             String url = toUrl(jarPath, (String) iter.next());
295             if (url != null)
296                 return url;
297         }
298 
299         return null;
300     }
301 
302     /**
303      * Returns a URL to the entry in the given JAr if both the JAR file and the
304      * entry in it actually exist. Otherwise returns null.
305      * 
306      * @param jarPath -
307      *            path to the JAR file to check
308      * @param entry -
309      *            path to the entry inside the JAr file to check
310      * @return - a valid URL to the entry inside the JAR or null if either the
311      *         JAr or the entry do not exist
312      */
313     private String toUrl(String jarPath, String entry)
314     {
315         try
316         {
317             if (new JarFile(jarPath).getEntry(entry) != null)
318             {
319                 return jarPath + "!/" + entry;
320             }
321             else
322             {
323                 if (getLog().isDebugEnabled())
324                     getLog().debug("Entry " + entry + " is not found in " + jarPath);
325                 return null;
326             }
327         }
328         catch (Exception e)
329         {
330             if (getLog().isDebugEnabled())
331                 getLog().debug("Path is invalid: " + jarPath, e);
332             return null;
333         }
334     }
335 
336     private Set resolveDependencies() throws MojoExecutionException
337     {
338         // this is a list of direct dependencies as specified in the POM
339         // excluding those with unwanted scopes
340         Set dependencies = new TreeSet(project.getDependencyArtifacts());
341 
342         // then we gather all modules as direct dependencies
343         for (Iterator i = reactorProjects.iterator(); i.hasNext();)
344         {
345             MavenProject subProject = (MavenProject) i.next();
346             if (subProject != project)
347             {
348                 getLog().info("Aggregating dependency for " + subProject.getName());
349                 dependencies.add(subProject.getArtifact());
350             }
351         }
352 
353         try
354         {
355             // now we need to resolve all the transitive dependencies too
356             ArtifactFilter filter = createExclusionFilter();
357             ArtifactResolutionResult result = resolver.resolveTransitively(dependencies, project.getArtifact(),
358                     localRepository, remoteRepositories, artifactMetadataSource, filter);
359             return result.getArtifacts();
360         }
361         catch (MalformedPatternException e)
362         {
363             throw new MojoExecutionException("Failed to parse an exclusion filter", e);
364         }
365         catch (ArtifactResolutionException e)
366         {
367             throw new MojoExecutionException("Failed to resolve transitively.", e);
368         }
369         catch (ArtifactNotFoundException e)
370         {
371             throw new MojoExecutionException("Failed to resolve transitively.", e);
372         }
373     }
374 
375     protected ArtifactExclusionFilter createExclusionFilter() throws MalformedPatternException
376     {
377         // make sure we have a non-null exclusion lists
378         validateExclusionList();
379         return new ArtifactExclusionFilter(excludedLibraryList, excludedScopeList);
380     }
381 
382     /**
383      * checks the List version and String version of the parameters and makes
384      * sure the List is not null
385      */
386     private void validateExclusionList()
387     {
388         this.excludedScopeList = fillListIfNull(excludedScopes, excludedScopeList);
389         this.excludedLibraryList = fillListIfNull(excludedLibraries, excludedLibraryList);
390     }
391 
392     private static List fillListIfNull(String csvString, List list)
393     {
394         if (list == null)
395         {
396             list = new ArrayList();
397             if (csvString != null && csvString.length() > 0)
398             {
399                 StringTokenizer tokenizer = new StringTokenizer(csvString, ",");
400                 while (tokenizer.hasMoreTokens())
401                     list.add(tokenizer.nextToken());
402             }
403         }
404         return list;
405     }
406 }