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 }