View Javadoc

1   /**
2    * Copyright 2010-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.codehaus.mojo.license;
17  
18  import org.apache.maven.artifact.Artifact;
19  import org.apache.maven.artifact.repository.ArtifactRepository;
20  import org.apache.maven.model.License;
21  import org.apache.maven.plugin.AbstractMojo;
22  import org.apache.maven.plugin.MojoExecutionException;
23  import org.apache.maven.project.MavenProject;
24  import org.codehaus.mojo.license.model.ProjectLicenseInfo;
25  
26  import java.io.File;
27  import java.io.FileInputStream;
28  import java.io.FileNotFoundException;
29  import java.io.IOException;
30  import java.net.MalformedURLException;
31  import java.net.URL;
32  import java.util.*;
33  
34  /**
35   * Download the license files of all the current project's dependencies, and generate a summary file containing a list
36   * of all dependencies and their licenses.
37   *
38   * @author Paul Gier
39   * @version $Revision: 14414 $
40   * @phase package
41   * @goal download-licenses
42   * @requiresDependencyResolution test
43   * @since 1.0
44   */
45  public class DownloadLicensesMojo
46      extends AbstractMojo
47      implements MavenProjectDependenciesConfigurator
48  {
49  
50      /**
51       * The Maven Project Object
52       *
53       * @parameter default-value="${project}"
54       * @readonly
55       * @since 1.0
56       */
57      private MavenProject project;
58  
59      /**
60       * Location of the local repository.
61       *
62       * @parameter default-value="${localRepository}"
63       * @readonly
64       * @since 1.0
65       */
66      private ArtifactRepository localRepository;
67  
68      /**
69       * List of Remote Repositories used by the resolver
70       *
71       * @parameter default-value="${project.remoteArtifactRepositories}"
72       * @readonly
73       * @since 1.0
74       */
75      private List remoteRepositories;
76  
77      /**
78       * Input file containing a mapping between each dependency and it's license information.
79       *
80       * @parameter default-value="${project.basedir}/src/license/licenses.xml" expression="${licensesConfigFile}"
81       * @since 1.0
82       */
83      private File licensesConfigFile;
84  
85      /**
86       * The directory to which the dependency licenses should be written.
87       *
88       * @parameter default-value="${project.build.directory}/generated-resources/licenses" expression="${licensesOutputDirectory}
89       * @since 1.0
90       */
91      private File licensesOutputDirectory;
92  
93      /**
94       * The output file containing a mapping between each dependency and it's license information.
95       *
96       * @parameter default-value="${project.build.directory}/generated-resources/licenses.xml" expression="${licensesOutputFile}
97       * @since 1.0
98       */
99      private File licensesOutputFile;
100 
101     /**
102      * A filter to exclude some scopes.
103      *
104      * @parameter expression="${license.excludedScopes}" default-value="system"
105      * @since 1.0
106      */
107     private String excludedScopes;
108 
109     /**
110      * A filter to include only some scopes, if let empty then all scopes will be used (no filter).
111      *
112      * @parameter expression="${license.includedScopes}" default-value=""
113      * @since 1.0
114      */
115     private String includedScopes;
116 
117     /**
118      * Settings offline flag (will not download anything if setted to true).
119      *
120      * @parameter default-value="${settings.offline}"
121      * @since 1.0
122      */
123     private boolean offline;
124 
125     /**
126      * Don't show warnings about bad or missing license files.
127      *
128      * @parameter default-value="false"
129      * @since 1.0
130      */
131     private boolean quiet;
132 
133     /**
134      * Include transitive dependencies when downloading license files.
135      *
136      * @parameter default-value="true"
137      * @since 1.0
138      */
139     private boolean includeTransitiveDependencies;
140 
141     /**
142      * dependencies tool.
143      *
144      * @component
145      * @readonly
146      * @since 1.0
147      */
148     private DependenciesTool dependenciesTool;
149 
150     /**
151      * Keeps a collection of the URLs of the licenses that have been downlaoded. This helps the plugin to avoid
152      * downloading the same license multiple times.
153      */
154     private Set<String> downloadedLicenseURLs = new HashSet<String>();
155 
156     /**
157      * Main Maven plugin execution
158      */
159     public void execute()
160         throws MojoExecutionException
161     {
162 
163         if ( offline )
164         {
165 
166             getLog().warn( "Offline flag is on, download-licenses goal is skip." );
167             return;
168         }
169         initDirectories();
170 
171         Map<String, ProjectLicenseInfo> configuredDepLicensesMap = new HashMap<String, ProjectLicenseInfo>();
172 
173         // License info from previous build
174         if ( licensesOutputFile.exists() )
175         {
176             loadLicenseInfo( configuredDepLicensesMap, licensesOutputFile, true );
177         }
178 
179         // Manually configured license info, loaded second to override previously loaded info
180         if ( licensesConfigFile.exists() )
181         {
182             loadLicenseInfo( configuredDepLicensesMap, licensesConfigFile, false );
183         }
184 
185         SortedMap<String, MavenProject> dependencies =
186             dependenciesTool.loadProjectDependencies( project, this, localRepository, remoteRepositories, null );
187 
188         // The resulting list of licenses after dependency resolution
189         List<ProjectLicenseInfo> depProjectLicenses = new ArrayList<ProjectLicenseInfo>();
190 
191         for ( MavenProject project : dependencies.values() )
192         {
193             Artifact artifact = project.getArtifact();
194             getLog().debug( "Checking licenses for project " + artifact );
195             String artifactProjectId = getArtifactProjectId( artifact );
196             ProjectLicenseInfo depProject;
197             if ( configuredDepLicensesMap.containsKey( artifactProjectId ) )
198             {
199                 depProject = configuredDepLicensesMap.get( artifactProjectId );
200                 depProject.setVersion( artifact.getVersion() );
201             }
202             else
203             {
204                 depProject = createDependencyProject( project );
205             }
206             downloadLicenses( depProject );
207             depProjectLicenses.add( depProject );
208         }
209 
210         try
211         {
212             LicenseSummaryWriter.writeLicenseSummary( depProjectLicenses, licensesOutputFile );
213         }
214         catch ( Exception e )
215         {
216             throw new MojoExecutionException( "Unable to write license summary file: " + licensesOutputFile, e );
217         }
218 
219     }
220 
221     private void initDirectories()
222         throws MojoExecutionException
223     {
224         try
225         {
226             FileUtil.createDirectoryIfNecessary( licensesOutputDirectory );
227 
228             FileUtil.createDirectoryIfNecessary( licensesOutputFile.getParentFile() );
229         }
230         catch ( IOException e )
231         {
232             throw new MojoExecutionException( "Unable to create a directory...", e );
233         }
234     }
235 
236     /**
237      * Load the license information contained in a file if it exists. Will overwrite existing license information in the
238      * map for dependencies with the same id. If the config file does not exist, the method does nothing.
239      *
240      * @param configuredDepLicensesMap A map between the dependencyId and the license info
241      * @param licenseConfigFile        The license configuration file to load
242      * @param previouslyDownloaded     Whether these licenses were already downloaded
243      * @throws MojoExecutionException if could not load license infos
244      */
245     private void loadLicenseInfo( Map<String, ProjectLicenseInfo> configuredDepLicensesMap, File licenseConfigFile,
246                                   boolean previouslyDownloaded )
247         throws MojoExecutionException
248     {
249         FileInputStream fis = null;
250         try
251         {
252             fis = new FileInputStream( licenseConfigFile );
253             List<ProjectLicenseInfo> licensesList = LicenseSummaryReader.parseLicenseSummary( fis );
254             for ( ProjectLicenseInfo dep : licensesList )
255             {
256                 configuredDepLicensesMap.put( dep.getId(), dep );
257                 if ( previouslyDownloaded )
258                 {
259                     for ( License license : dep.getLicenses() )
260                     {
261                         // Save the URL so we don't download it again
262                         downloadedLicenseURLs.add( license.getUrl() );
263                     }
264                 }
265             }
266         }
267         catch ( Exception e )
268         {
269             throw new MojoExecutionException( "Unable to parse license summary output file: " + licenseConfigFile, e );
270         }
271         finally
272         {
273             FileUtil.tryClose( fis );
274         }
275     }
276 
277     /**
278      * Returns the project ID for the artifact
279      *
280      * @param artifact the artifact
281      * @return groupId:artifactId
282      */
283     public String getArtifactProjectId( Artifact artifact )
284     {
285         return artifact.getGroupId() + ":" + artifact.getArtifactId();
286     }
287 
288     /**
289      * Create a simple DependencyProject object containing the GAV and license info from the Maven Artifact
290      *
291      * @param depMavenProject the dependency maven project
292      * @return DependencyProject with artifact and license info
293      */
294     public ProjectLicenseInfo createDependencyProject( MavenProject depMavenProject )
295     {
296         ProjectLicenseInfo dependencyProject =
297             new ProjectLicenseInfo( depMavenProject.getGroupId(), depMavenProject.getArtifactId(),
298                                     depMavenProject.getVersion() );
299         List<?> licenses = depMavenProject.getLicenses();
300         for ( Object license : licenses )
301         {
302             dependencyProject.addLicense( (License) license );
303         }
304         return dependencyProject;
305     }
306 
307     /**
308      * Determine filename to use for downloaded license file. The file name is based on the configured name of the
309      * license (if available) and the remote filename of the license.
310      *
311      * @param license the license
312      * @return A filename to be used for the downloaded license file
313      * @throws MalformedURLException if the license url is malformed
314      */
315     private String getLicenseFileName( License license )
316         throws MalformedURLException
317     {
318         URL licenseUrl = new URL( license.getUrl() );
319         File licenseUrlFile = new File( licenseUrl.getPath() );
320         String licenseFileName = licenseUrlFile.getName();
321 
322         if ( license.getName() != null )
323         {
324             licenseFileName = license.getName() + " - " + licenseUrlFile.getName();
325         }
326 
327         // Check if the file has a valid file extention
328         final String DEFAULT_EXTENSION = ".txt";
329         int extensionIndex = licenseFileName.lastIndexOf( "." );
330         if ( extensionIndex == -1 || extensionIndex > ( licenseFileName.length() - 3 ) )
331         {
332             // This means it isn't a valid file extension, so append the default
333             licenseFileName = licenseFileName + DEFAULT_EXTENSION;
334         }
335 
336         // Force lower case so we don't end up with multiple copies of the same license
337         licenseFileName = licenseFileName.toLowerCase();
338 
339         return licenseFileName;
340     }
341 
342     /**
343      * Download the licenses associated with this project
344      *
345      * @param depProject The project which generated the dependency
346      */
347     private void downloadLicenses( ProjectLicenseInfo depProject )
348     {
349         getLog().debug( "Downloading license(s) for project " + depProject );
350 
351         List<License> licenses = depProject.getLicenses();
352 
353         if ( depProject.getLicenses() == null || depProject.getLicenses().isEmpty() )
354         {
355             if ( !quiet )
356             {
357                 getLog().warn( "No license information available for: " + depProject );
358             }
359             return;
360         }
361 
362         for ( License license : licenses )
363         {
364             try
365             {
366                 String licenseFileName = getLicenseFileName( license );
367 
368                 File licenseOutputFile = new File( licensesOutputDirectory, licenseFileName );
369                 if ( licenseOutputFile.exists() )
370                 {
371                     continue;
372                 }
373 
374                 if ( !downloadedLicenseURLs.contains( license.getUrl() ) )
375                 {
376                     LicenseDownloader.downloadLicense( license.getUrl(), licenseOutputFile );
377                     downloadedLicenseURLs.add( license.getUrl() );
378                 }
379             }
380             catch ( MalformedURLException e )
381             {
382                 if ( !quiet )
383                 {
384                     getLog().warn( "POM for dependency " + depProject.toString() + " has an invalid license URL: " +
385                                        license.getUrl() );
386                 }
387             }
388             catch ( FileNotFoundException e )
389             {
390                 if ( !quiet )
391                 {
392                     getLog().warn( "POM for dependency " + depProject.toString() +
393                                        " has a license URL that returns file not found: " + license.getUrl() );
394                 }
395             }
396             catch ( IOException e )
397             {
398                 getLog().warn( "Unable to retrieve license for dependency: " + depProject.toString() );
399                 getLog().warn( license.getUrl() );
400                 getLog().warn( e.getMessage() );
401             }
402 
403         }
404 
405     }
406 
407     public MavenProject getProject()
408     {
409         return project;
410     }
411 
412     public ArtifactRepository getLocalRepository()
413     {
414         return localRepository;
415     }
416 
417     public List getRemoteRepositories()
418     {
419         return remoteRepositories;
420     }
421 
422     /**
423      * {@inheritDoc}
424      */
425     public boolean isIncludeTransitiveDependencies()
426     {
427         return includeTransitiveDependencies;
428     }
429 
430     /**
431      * {@inheritDoc}
432      */
433     public List<String> getExcludedScopes()
434     {
435         String[] split = excludedScopes == null ? new String[0] : excludedScopes.split( "," );
436         return Arrays.asList( split );
437     }
438 
439     public void setExcludedScopes( String excludedScopes )
440     {
441         this.excludedScopes = excludedScopes;
442     }
443 
444     /**
445      * {@inheritDoc}
446      */
447     public List<String> getIncludedScopes()
448     {
449         String[] split = includedScopes == null ? new String[0] : includedScopes.split( "," );
450         return Arrays.asList( split );
451     }
452 
453     public void setIncludedScopes( String includedScopes )
454     {
455         this.includedScopes = includedScopes;
456     }
457 
458     // not used at the moment
459 
460     /**
461      * {@inheritDoc}
462      */
463     public String getIncludedArtifacts()
464     {
465         return null;
466     }
467 
468     // not used at the moment
469 
470     /**
471      * {@inheritDoc}
472      */
473     public String getIncludedGroups()
474     {
475         return null;
476     }
477 
478     // not used at the moment
479 
480     /**
481      * {@inheritDoc}
482      */
483     public String getExcludedGroups()
484     {
485         return null;
486     }
487 
488     // not used at the moment
489 
490     /**
491      * {@inheritDoc}
492      */
493     public String getExcludedArtifacts()
494     {
495         return null;
496     }
497 
498     /**
499      * {@inheritDoc}
500      */
501     public boolean isVerbose()
502     {
503         return getLog().isDebugEnabled();
504     }
505 }