001    /*
002     * #%L
003     * License Maven Plugin
004     *
005     * $Id: DownloadLicensesMojo.java 14414 2011-08-10 23:10:24Z tchemit $
006     * $HeadURL: http://svn.codehaus.org/mojo/tags/license-maven-plugin-1.0/src/main/java/org/codehaus/mojo/license/DownloadLicensesMojo.java $
007     * %%
008     * Copyright (C) 2010 - 2011 CodeLutin, Codehaus, Tony Chemit
009     * %%
010     * This program is free software: you can redistribute it and/or modify
011     * it under the terms of the GNU Lesser General Public License as 
012     * published by the Free Software Foundation, either version 3 of the 
013     * License, or (at your option) any later version.
014     * 
015     * This program is distributed in the hope that it will be useful,
016     * but WITHOUT ANY WARRANTY; without even the implied warranty of
017     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
018     * GNU General Lesser Public License for more details.
019     * 
020     * You should have received a copy of the GNU General Lesser Public 
021     * License along with this program.  If not, see
022     * <http://www.gnu.org/licenses/lgpl-3.0.html>.
023     * #L%
024     */
025    package org.codehaus.mojo.license;
026    
027    import org.apache.maven.artifact.Artifact;
028    import org.apache.maven.artifact.repository.ArtifactRepository;
029    import org.apache.maven.model.License;
030    import org.apache.maven.plugin.AbstractMojo;
031    import org.apache.maven.plugin.MojoExecutionException;
032    import org.apache.maven.project.MavenProject;
033    import org.codehaus.mojo.license.model.ProjectLicenseInfo;
034    
035    import java.io.File;
036    import java.io.FileInputStream;
037    import java.io.FileNotFoundException;
038    import java.io.IOException;
039    import java.net.MalformedURLException;
040    import java.net.URL;
041    import java.util.*;
042    
043    /**
044     * Download the license files of all the current project's dependencies, and generate a summary file containing a list
045     * of all dependencies and their licenses.
046     *
047     * @author Paul Gier
048     * @version $Revision: 14414 $
049     * @phase package
050     * @goal download-licenses
051     * @requiresDependencyResolution test
052     * @since 1.0
053     */
054    public class DownloadLicensesMojo
055        extends AbstractMojo
056        implements MavenProjectDependenciesConfigurator
057    {
058    
059        /**
060         * The Maven Project Object
061         *
062         * @parameter default-value="${project}"
063         * @readonly
064         * @since 1.0
065         */
066        private MavenProject project;
067    
068        /**
069         * Location of the local repository.
070         *
071         * @parameter default-value="${localRepository}"
072         * @readonly
073         * @since 1.0
074         */
075        private ArtifactRepository localRepository;
076    
077        /**
078         * List of Remote Repositories used by the resolver
079         *
080         * @parameter default-value="${project.remoteArtifactRepositories}"
081         * @readonly
082         * @since 1.0
083         */
084        private List remoteRepositories;
085    
086        /**
087         * Input file containing a mapping between each dependency and it's license information.
088         *
089         * @parameter default-value="${project.basedir}/src/license/licenses.xml" expression="${licensesConfigFile}"
090         * @since 1.0
091         */
092        private File licensesConfigFile;
093    
094        /**
095         * The directory to which the dependency licenses should be written.
096         *
097         * @parameter default-value="${project.build.directory}/generated-resources/licenses" expression="${licensesOutputDirectory}
098         * @since 1.0
099         */
100        private File licensesOutputDirectory;
101    
102        /**
103         * The output file containing a mapping between each dependency and it's license information.
104         *
105         * @parameter default-value="${project.build.directory}/generated-resources/licenses.xml" expression="${licensesOutputFile}
106         * @since 1.0
107         */
108        private File licensesOutputFile;
109    
110        /**
111         * A filter to exclude some scopes.
112         *
113         * @parameter expression="${license.excludedScopes}" default-value="system"
114         * @since 1.0
115         */
116        private String excludedScopes;
117    
118        /**
119         * A filter to include only some scopes, if let empty then all scopes will be used (no filter).
120         *
121         * @parameter expression="${license.includedScopes}" default-value=""
122         * @since 1.0
123         */
124        private String includedScopes;
125    
126        /**
127         * Settings offline flag (will not download anything if setted to true).
128         *
129         * @parameter default-value="${settings.offline}"
130         * @since 1.0
131         */
132        private boolean offline;
133    
134        /**
135         * Don't show warnings about bad or missing license files.
136         *
137         * @parameter default-value="false"
138         * @since 1.0
139         */
140        private boolean quiet;
141    
142        /**
143         * Include transitive dependencies when downloading license files.
144         *
145         * @parameter default-value="true"
146         * @since 1.0
147         */
148        private boolean includeTransitiveDependencies;
149    
150        /**
151         * dependencies tool.
152         *
153         * @component
154         * @readonly
155         * @since 1.0
156         */
157        private DependenciesTool dependenciesTool;
158    
159        /**
160         * Keeps a collection of the URLs of the licenses that have been downlaoded. This helps the plugin to avoid
161         * downloading the same license multiple times.
162         */
163        private Set<String> downloadedLicenseURLs = new HashSet<String>();
164    
165        /**
166         * Main Maven plugin execution
167         */
168        public void execute()
169            throws MojoExecutionException
170        {
171    
172            if ( offline )
173            {
174    
175                getLog().warn( "Offline flag is on, download-licenses goal is skip." );
176                return;
177            }
178            initDirectories();
179    
180            Map<String, ProjectLicenseInfo> configuredDepLicensesMap = new HashMap<String, ProjectLicenseInfo>();
181    
182            // License info from previous build
183            if ( licensesOutputFile.exists() )
184            {
185                loadLicenseInfo( configuredDepLicensesMap, licensesOutputFile, true );
186            }
187    
188            // Manually configured license info, loaded second to override previously loaded info
189            if ( licensesConfigFile.exists() )
190            {
191                loadLicenseInfo( configuredDepLicensesMap, licensesConfigFile, false );
192            }
193    
194            SortedMap<String, MavenProject> dependencies =
195                dependenciesTool.loadProjectDependencies( project, this, localRepository, remoteRepositories, null );
196    
197            // The resulting list of licenses after dependency resolution
198            List<ProjectLicenseInfo> depProjectLicenses = new ArrayList<ProjectLicenseInfo>();
199    
200            for ( MavenProject project : dependencies.values() )
201            {
202                Artifact artifact = project.getArtifact();
203                getLog().debug( "Checking licenses for project " + artifact );
204                String artifactProjectId = getArtifactProjectId( artifact );
205                ProjectLicenseInfo depProject;
206                if ( configuredDepLicensesMap.containsKey( artifactProjectId ) )
207                {
208                    depProject = configuredDepLicensesMap.get( artifactProjectId );
209                    depProject.setVersion( artifact.getVersion() );
210                }
211                else
212                {
213                    depProject = createDependencyProject( project );
214                }
215                downloadLicenses( depProject );
216                depProjectLicenses.add( depProject );
217            }
218    
219            try
220            {
221                LicenseSummaryWriter.writeLicenseSummary( depProjectLicenses, licensesOutputFile );
222            }
223            catch ( Exception e )
224            {
225                throw new MojoExecutionException( "Unable to write license summary file: " + licensesOutputFile, e );
226            }
227    
228        }
229    
230        private void initDirectories()
231            throws MojoExecutionException
232        {
233            try
234            {
235                FileUtil.createDirectoryIfNecessary( licensesOutputDirectory );
236    
237                FileUtil.createDirectoryIfNecessary( licensesOutputFile.getParentFile() );
238            }
239            catch ( IOException e )
240            {
241                throw new MojoExecutionException( "Unable to create a directory...", e );
242            }
243        }
244    
245        /**
246         * Load the license information contained in a file if it exists. Will overwrite existing license information in the
247         * map for dependencies with the same id. If the config file does not exist, the method does nothing.
248         *
249         * @param configuredDepLicensesMap A map between the dependencyId and the license info
250         * @param licenseConfigFile        The license configuration file to load
251         * @param previouslyDownloaded     Whether these licenses were already downloaded
252         * @throws MojoExecutionException if could not load license infos
253         */
254        private void loadLicenseInfo( Map<String, ProjectLicenseInfo> configuredDepLicensesMap, File licenseConfigFile,
255                                      boolean previouslyDownloaded )
256            throws MojoExecutionException
257        {
258            FileInputStream fis = null;
259            try
260            {
261                fis = new FileInputStream( licenseConfigFile );
262                List<ProjectLicenseInfo> licensesList = LicenseSummaryReader.parseLicenseSummary( fis );
263                for ( ProjectLicenseInfo dep : licensesList )
264                {
265                    configuredDepLicensesMap.put( dep.getId(), dep );
266                    if ( previouslyDownloaded )
267                    {
268                        for ( License license : dep.getLicenses() )
269                        {
270                            // Save the URL so we don't download it again
271                            downloadedLicenseURLs.add( license.getUrl() );
272                        }
273                    }
274                }
275            }
276            catch ( Exception e )
277            {
278                throw new MojoExecutionException( "Unable to parse license summary output file: " + licenseConfigFile, e );
279            }
280            finally
281            {
282                FileUtil.tryClose( fis );
283            }
284        }
285    
286        /**
287         * Returns the project ID for the artifact
288         *
289         * @param artifact the artifact
290         * @return groupId:artifactId
291         */
292        public String getArtifactProjectId( Artifact artifact )
293        {
294            return artifact.getGroupId() + ":" + artifact.getArtifactId();
295        }
296    
297        /**
298         * Create a simple DependencyProject object containing the GAV and license info from the Maven Artifact
299         *
300         * @param depMavenProject the dependency maven project
301         * @return DependencyProject with artifact and license info
302         */
303        public ProjectLicenseInfo createDependencyProject( MavenProject depMavenProject )
304        {
305            ProjectLicenseInfo dependencyProject =
306                new ProjectLicenseInfo( depMavenProject.getGroupId(), depMavenProject.getArtifactId(),
307                                        depMavenProject.getVersion() );
308            List<?> licenses = depMavenProject.getLicenses();
309            for ( Object license : licenses )
310            {
311                dependencyProject.addLicense( (License) license );
312            }
313            return dependencyProject;
314        }
315    
316        /**
317         * Determine filename to use for downloaded license file. The file name is based on the configured name of the
318         * license (if available) and the remote filename of the license.
319         *
320         * @param license the license
321         * @return A filename to be used for the downloaded license file
322         * @throws MalformedURLException if the license url is malformed
323         */
324        private String getLicenseFileName( License license )
325            throws MalformedURLException
326        {
327            URL licenseUrl = new URL( license.getUrl() );
328            File licenseUrlFile = new File( licenseUrl.getPath() );
329            String licenseFileName = licenseUrlFile.getName();
330    
331            if ( license.getName() != null )
332            {
333                licenseFileName = license.getName() + " - " + licenseUrlFile.getName();
334            }
335    
336            // Check if the file has a valid file extention
337            final String DEFAULT_EXTENSION = ".txt";
338            int extensionIndex = licenseFileName.lastIndexOf( "." );
339            if ( extensionIndex == -1 || extensionIndex > ( licenseFileName.length() - 3 ) )
340            {
341                // This means it isn't a valid file extension, so append the default
342                licenseFileName = licenseFileName + DEFAULT_EXTENSION;
343            }
344    
345            // Force lower case so we don't end up with multiple copies of the same license
346            licenseFileName = licenseFileName.toLowerCase();
347    
348            return licenseFileName;
349        }
350    
351        /**
352         * Download the licenses associated with this project
353         *
354         * @param depProject The project which generated the dependency
355         */
356        private void downloadLicenses( ProjectLicenseInfo depProject )
357        {
358            getLog().debug( "Downloading license(s) for project " + depProject );
359    
360            List<License> licenses = depProject.getLicenses();
361    
362            if ( depProject.getLicenses() == null || depProject.getLicenses().isEmpty() )
363            {
364                if ( !quiet )
365                {
366                    getLog().warn( "No license information available for: " + depProject );
367                }
368                return;
369            }
370    
371            for ( License license : licenses )
372            {
373                try
374                {
375                    String licenseFileName = getLicenseFileName( license );
376    
377                    File licenseOutputFile = new File( licensesOutputDirectory, licenseFileName );
378                    if ( licenseOutputFile.exists() )
379                    {
380                        continue;
381                    }
382    
383                    if ( !downloadedLicenseURLs.contains( license.getUrl() ) )
384                    {
385                        LicenseDownloader.downloadLicense( license.getUrl(), licenseOutputFile );
386                        downloadedLicenseURLs.add( license.getUrl() );
387                    }
388                }
389                catch ( MalformedURLException e )
390                {
391                    if ( !quiet )
392                    {
393                        getLog().warn( "POM for dependency " + depProject.toString() + " has an invalid license URL: " +
394                                           license.getUrl() );
395                    }
396                }
397                catch ( FileNotFoundException e )
398                {
399                    if ( !quiet )
400                    {
401                        getLog().warn( "POM for dependency " + depProject.toString() +
402                                           " has a license URL that returns file not found: " + license.getUrl() );
403                    }
404                }
405                catch ( IOException e )
406                {
407                    getLog().warn( "Unable to retrieve license for dependency: " + depProject.toString() );
408                    getLog().warn( license.getUrl() );
409                    getLog().warn( e.getMessage() );
410                }
411    
412            }
413    
414        }
415    
416        public MavenProject getProject()
417        {
418            return project;
419        }
420    
421        public ArtifactRepository getLocalRepository()
422        {
423            return localRepository;
424        }
425    
426        public List getRemoteRepositories()
427        {
428            return remoteRepositories;
429        }
430    
431        /**
432         * {@inheritDoc}
433         */
434        public boolean isIncludeTransitiveDependencies()
435        {
436            return includeTransitiveDependencies;
437        }
438    
439        /**
440         * {@inheritDoc}
441         */
442        public List<String> getExcludedScopes()
443        {
444            String[] split = excludedScopes == null ? new String[0] : excludedScopes.split( "," );
445            return Arrays.asList( split );
446        }
447    
448        public void setExcludedScopes( String excludedScopes )
449        {
450            this.excludedScopes = excludedScopes;
451        }
452    
453        /**
454         * {@inheritDoc}
455         */
456        public List<String> getIncludedScopes()
457        {
458            String[] split = includedScopes == null ? new String[0] : includedScopes.split( "," );
459            return Arrays.asList( split );
460        }
461    
462        public void setIncludedScopes( String includedScopes )
463        {
464            this.includedScopes = includedScopes;
465        }
466    
467        // not used at the moment
468    
469        /**
470         * {@inheritDoc}
471         */
472        public String getIncludedArtifacts()
473        {
474            return null;
475        }
476    
477        // not used at the moment
478    
479        /**
480         * {@inheritDoc}
481         */
482        public String getIncludedGroups()
483        {
484            return null;
485        }
486    
487        // not used at the moment
488    
489        /**
490         * {@inheritDoc}
491         */
492        public String getExcludedGroups()
493        {
494            return null;
495        }
496    
497        // not used at the moment
498    
499        /**
500         * {@inheritDoc}
501         */
502        public String getExcludedArtifacts()
503        {
504            return null;
505        }
506    
507        /**
508         * {@inheritDoc}
509         */
510        public boolean isVerbose()
511        {
512            return getLog().isDebugEnabled();
513        }
514    }