001    /**
002     * Copyright 2010-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.codehaus.mojo.license;
017    
018    import org.apache.maven.artifact.Artifact;
019    import org.apache.maven.artifact.repository.ArtifactRepository;
020    import org.apache.maven.model.License;
021    import org.apache.maven.plugin.AbstractMojo;
022    import org.apache.maven.plugin.MojoExecutionException;
023    import org.apache.maven.project.MavenProject;
024    import org.codehaus.mojo.license.model.ProjectLicenseInfo;
025    
026    import java.io.File;
027    import java.io.FileInputStream;
028    import java.io.FileNotFoundException;
029    import java.io.IOException;
030    import java.net.MalformedURLException;
031    import java.net.URL;
032    import java.util.*;
033    
034    /**
035     * Download the license files of all the current project's dependencies, and generate a summary file containing a list
036     * of all dependencies and their licenses.
037     *
038     * @author Paul Gier
039     * @version $Revision: 14414 $
040     * @phase package
041     * @goal download-licenses
042     * @requiresDependencyResolution test
043     * @since 1.0
044     */
045    public class DownloadLicensesMojo
046        extends AbstractMojo
047        implements MavenProjectDependenciesConfigurator
048    {
049    
050        /**
051         * The Maven Project Object
052         *
053         * @parameter default-value="${project}"
054         * @readonly
055         * @since 1.0
056         */
057        private MavenProject project;
058    
059        /**
060         * Location of the local repository.
061         *
062         * @parameter default-value="${localRepository}"
063         * @readonly
064         * @since 1.0
065         */
066        private ArtifactRepository localRepository;
067    
068        /**
069         * List of Remote Repositories used by the resolver
070         *
071         * @parameter default-value="${project.remoteArtifactRepositories}"
072         * @readonly
073         * @since 1.0
074         */
075        private List remoteRepositories;
076    
077        /**
078         * Input file containing a mapping between each dependency and it's license information.
079         *
080         * @parameter default-value="${project.basedir}/src/license/licenses.xml" expression="${licensesConfigFile}"
081         * @since 1.0
082         */
083        private File licensesConfigFile;
084    
085        /**
086         * The directory to which the dependency licenses should be written.
087         *
088         * @parameter default-value="${project.build.directory}/generated-resources/licenses" expression="${licensesOutputDirectory}
089         * @since 1.0
090         */
091        private File licensesOutputDirectory;
092    
093        /**
094         * The output file containing a mapping between each dependency and it's license information.
095         *
096         * @parameter default-value="${project.build.directory}/generated-resources/licenses.xml" expression="${licensesOutputFile}
097         * @since 1.0
098         */
099        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    }