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 }