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 java.io.File; 019 import java.io.FileInputStream; 020 import java.io.IOException; 021 import java.io.InputStream; 022 import java.io.OutputStream; 023 import java.util.ArrayList; 024 import java.util.Arrays; 025 import java.util.Collections; 026 import java.util.HashMap; 027 import java.util.List; 028 import java.util.Map; 029 import java.util.SortedMap; 030 import java.util.SortedSet; 031 import java.util.TreeMap; 032 033 import org.apache.commons.collections.CollectionUtils; 034 import org.apache.commons.io.FileUtils; 035 import org.apache.commons.io.IOUtils; 036 import org.apache.commons.lang.StringUtils; 037 import org.apache.maven.plugin.MojoFailureException; 038 import org.apache.maven.plugin.logging.Log; 039 import org.apache.maven.project.MavenProject; 040 import org.apache.maven.project.ProjectBuildingException; 041 import org.codehaus.mojo.license.model.LicenseMap; 042 import org.springframework.core.io.DefaultResourceLoader; 043 import org.springframework.core.io.Resource; 044 import org.springframework.core.io.ResourceLoader; 045 046 /** 047 * Abstract mojo for all third-party mojos. 048 * 049 * @author tchemit <chemit@codelutin.com> 050 * @since 1.0 051 */ 052 public abstract class AbstractAddThirdPartyMojo extends AbstractLicenseMojo { 053 054 /** 055 * Directory where to generate files. 056 * 057 * @parameter expression="${license.outputDirectory}" 058 * default-value="${project.build.directory}/generated-sources/license" 059 * @required 060 * @since 1.0 061 */ 062 protected File outputDirectory; 063 064 /** 065 * File where license information for third party dependencies gets stored 066 * 067 * @parameter expression="${license.thirdPartyFilename}" default-value="THIRD-PARTY.txt" 068 * @required 069 * @since 1.0 070 */ 071 protected String thirdPartyFilename; 072 073 /** 074 * A flag to use the missing licenses file to consolidate the THID-PARTY file. 075 * 076 * @parameter expression="${license.useMissingFile}" default-value="false" 077 * @since 1.0 078 */ 079 protected boolean useMissingFile; 080 081 /** 082 * The file where to fill the license for dependencies with unknown license. 083 * 084 * @parameter expression="${license.missingFile}" default-value="src/license/THIRD-PARTY.properties" 085 * @since 1.0 086 */ 087 protected File missingFile; 088 089 /** 090 * Location of a properties file mapping artifacts without a license to the license that should be used for them. 091 * This supports classpath notation and any other type of URL Spring 3.1 resource loading can understand. 092 * 093 * @parameter expression="${license.artifactLicenseMapping}" default-value="classpath:THIRD-PARTY.properties" 094 * @since 1.0 095 */ 096 protected String artifactLicenseMapping; 097 098 /** 099 * To merge licenses in final file. 100 * <p/> 101 * Each entry represents a merge (first license is main license to keep), licenses are separated by {@code |}. 102 * <p/> 103 * Example : 104 * <p/> 105 * 106 * <pre> 107 * <licenseMerges> 108 * <licenseMerge>The Apache Software License|Version 2.0,Apache License, Version 2.0</licenseMerge> 109 * </licenseMerges> 110 * </pre> 111 * 112 * @parameter 113 * @since 1.0 114 */ 115 protected List<String> licenseMerges; 116 117 /** 118 * The path of the bundled third party file to produce when {@link #generateBundle} is on. 119 * <p/> 120 * <b>Note:</b> This option is not available for {@code pom} module types. 121 * 122 * @parameter expression="${license.bundleThirdPartyPath}" 123 * default-value="META-INF/${project.artifactId}-THIRD-PARTY.txt" 124 * @since 1.0 125 */ 126 protected String bundleThirdPartyPath; 127 128 /** 129 * A flag to copy a bundled version of the third-party file. This is usefull to avoid for a final application 130 * collision name of third party file. 131 * <p/> 132 * The file will be copied at the {@link #bundleThirdPartyPath} location. 133 * 134 * @parameter expression="${license.generateBundle}" default-value="false" 135 * @since 1.0 136 */ 137 protected boolean generateBundle; 138 139 /** 140 * To force generation of the third-party file even if every thing is up to date. 141 * 142 * @parameter expression="${license.force}" default-value="false" 143 * @since 1.0 144 */ 145 protected boolean force; 146 147 /** 148 * A flag to fail the build if at least one dependency was detected without a license. 149 * 150 * @parameter expression="${license.failIfWarning}" default-value="false" 151 * @since 1.0 152 */ 153 protected boolean failIfWarning; 154 155 /** 156 * A flag to change the grouping of the generated THIRD-PARTY file. 157 * <p/> 158 * By default, group by dependencies. 159 * <p/> 160 * If sets to {@code true}, the it will group by license type. 161 * 162 * @parameter expression="${license.groupByLicense}" default-value="false" 163 * @since 1.0 164 */ 165 protected boolean groupByLicense; 166 167 /** 168 * A filter to exclude some scopes. 169 * 170 * @parameter expression="${license.excludedScopes}" default-value="system" 171 * @since 1.0 172 */ 173 protected String excludedScopes; 174 175 /** 176 * A filter to include only some scopes, if let empty then all scopes will be used (no filter). 177 * 178 * @parameter expression="${license.includedScopes}" default-value="" 179 * @since 1.0 180 */ 181 protected String includedScopes; 182 183 /** 184 * A filter to exclude some GroupIds 185 * 186 * @parameter expression="${license.excludedGroups}" default-value="" 187 * @since 1.0 188 */ 189 protected String excludedGroups; 190 191 /** 192 * A filter to include only some GroupIds 193 * 194 * @parameter expression="${license.includedGroups}" default-value="" 195 * @since 1.0 196 */ 197 protected String includedGroups; 198 199 /** 200 * A filter to exclude some ArtifactsIds 201 * 202 * @parameter expression="${license.excludedArtifacts}" default-value="" 203 * @since 1.0 204 */ 205 protected String excludedArtifacts; 206 207 /** 208 * A filter to include only some ArtifactsIds 209 * 210 * @parameter expression="${license.includedArtifacts}" default-value="" 211 * @since 1.0 212 */ 213 protected String includedArtifacts; 214 215 /** 216 * Include transitive dependencies when downloading license files. 217 * 218 * @parameter default-value="true" 219 * @since 1.0 220 */ 221 protected boolean includeTransitiveDependencies; 222 223 /** 224 * third party tool. 225 * 226 * @component 227 * @readonly 228 * @since 1.0 229 */ 230 private ThirdPartyTool thirdPartyTool; 231 232 private SortedMap<String, MavenProject> projectDependencies; 233 234 private LicenseMap licenseMap; 235 236 private SortedSet<MavenProject> unsafeDependencies; 237 238 private File thirdPartyFile; 239 240 private SortedProperties unsafeMappings; 241 242 private boolean doGenerate; 243 244 private boolean doGenerateBundle; 245 246 public static final String NO_DEPENDENCIES_MESSAGE = "the project has no dependencies."; 247 248 private static SortedMap<String, MavenProject> artifactCache; 249 250 public static SortedMap<String, MavenProject> getArtifactCache() { 251 if (artifactCache == null) { 252 artifactCache = new TreeMap<String, MavenProject>(); 253 } 254 return artifactCache; 255 } 256 257 protected abstract SortedMap<String, MavenProject> loadDependencies(); 258 259 protected abstract SortedProperties createUnsafeMapping() throws ProjectBuildingException, IOException, 260 ThirdPartyToolException; 261 262 protected boolean exists(String location) { 263 if (StringUtils.isBlank(location)) { 264 return false; 265 } 266 File file = new File(location); 267 if (file.exists()) { 268 return true; 269 } 270 ResourceLoader loader = new DefaultResourceLoader(); 271 Resource resource = loader.getResource(location); 272 return resource.exists(); 273 } 274 275 protected InputStream getInputStream(String location) throws IOException { 276 File file = new File(location); 277 if (file.exists()) { 278 return new FileInputStream(file); 279 } 280 ResourceLoader loader = new DefaultResourceLoader(); 281 Resource resource = loader.getResource(location); 282 if (!resource.exists()) { 283 throw new IllegalArgumentException("Can't open an input stream for " + location); 284 } else { 285 return resource.getInputStream(); 286 } 287 } 288 289 protected File copyToFileSystem(String location) { 290 File temp = new File(getProject().getBuild().getDirectory() + "/license/THIRD-PARTY.properties"); 291 return copyToFileSystem(location, temp); 292 } 293 294 protected File copyToFileSystem(String location, File file) { 295 InputStream in = null; 296 OutputStream out = null; 297 try { 298 in = getInputStream(location); 299 out = FileUtils.openOutputStream(file); 300 IOUtils.copy(in, out); 301 getLog().debug("Copied " + location + " to " + file); 302 return file; 303 } catch (IOException e) { 304 throw new IllegalArgumentException(e); 305 } finally { 306 IOUtils.closeQuietly(in); 307 IOUtils.closeQuietly(out); 308 } 309 } 310 311 @Override 312 protected void init() throws Exception { 313 if (exists(getArtifactLicenseMapping())) { 314 // The artifact->license mapping file might be a URL, not a file 315 // This call always copies the mapping file from wherever it is to target/license/THIRD-PARTY.properties 316 // This way we are guaranteed to have a local copy of the mapping file to work with 317 File propertiesFile = copyToFileSystem(getArtifactLicenseMapping()); 318 // "missingFile" contains a mapping between Maven GAV's and their corresponding license 319 setMissingFile(propertiesFile); 320 } 321 322 Log log = getLog(); 323 if (log.isDebugEnabled()) { 324 // always be verbose in debug mode 325 setVerbose(true); 326 } 327 328 // This is the file that gets bundled into the jar as META-INF/THIRD-PARTY.txt 329 // It contains the aggregated list of licenses/jar's this project depends on 330 File file = new File(getOutputDirectory(), getThirdPartyFilename()); 331 332 setThirdPartyFile(file); 333 334 long buildTimestamp = getBuildTimestamp(); 335 336 if (isVerbose()) { 337 log.info("Build start at : " + buildTimestamp); 338 log.info("third-party file : " + file.lastModified()); 339 } 340 341 setDoGenerate(isForce() || !file.exists() || buildTimestamp > file.lastModified()); 342 343 if (isGenerateBundle()) { 344 File bundleFile = FileUtil.getFile(getOutputDirectory(), getBundleThirdPartyPath()); 345 if (isVerbose()) { 346 log.info("bundle third-party file : " + bundleFile.lastModified()); 347 } 348 setDoGenerateBundle(isForce() || !bundleFile.exists() || buildTimestamp > bundleFile.lastModified()); 349 } else { 350 // not generating bundled file 351 setDoGenerateBundle(false); 352 } 353 354 // This is the complete, transitive list of dependencies of the current project 355 // It is stored as a map of MavenProjects keyed by GAV 356 // If the pom of the dep. includes the license(s) it is released under, project.getLicenses() returns that info 357 projectDependencies = loadDependencies(); 358 359 // This is also the complete, transitive list of dependencies of the current project 360 // However, it is stored as a map of Set<MavenProject>, where the key is the license name 361 licenseMap = createLicenseMap(projectDependencies); 362 363 // These are the dependencies whose pom's don't include license info 364 SortedSet<MavenProject> unsafeDependencies = getThirdPartyTool().getProjectsWithNoLicense(licenseMap, 365 isVerbose()); 366 367 setUnsafeDependencies(unsafeDependencies); 368 369 if (!CollectionUtils.isEmpty(unsafeDependencies) && isUseMissingFile() && isDoGenerate()) { 370 // load unsafeMapping 371 unsafeMappings = createUnsafeMapping(); 372 } 373 374 if (!CollectionUtils.isEmpty(licenseMerges)) { 375 376 // check where is not multi licenses merged main licenses (see OJO-1723) 377 Map<String, String[]> mergedLicenses = new HashMap<String, String[]>(); 378 379 for (String merge : licenseMerges) { 380 merge = merge.trim(); 381 String[] split = merge.split("\\|"); 382 383 String mainLicense = split[0]; 384 385 if (mergedLicenses.containsKey(mainLicense)) { 386 387 // this license was already describe, fail the build... 388 389 throw new MojoFailureException( 390 "The merge main license " 391 + mainLicense 392 + " was already registred in the " 393 + "configuration, please use only one such entry as describe in example " 394 + "http://mojo.codehaus.org/license-maven-plugin/examples/example-thirdparty.html#Merge_licenses."); 395 } 396 mergedLicenses.put(mainLicense, split); 397 } 398 399 // merge licenses in license map 400 401 for (String[] mergedLicense : mergedLicenses.values()) { 402 if (isVerbose()) { 403 getLog().info("Will merge " + Arrays.toString(mergedLicense) + ""); 404 } 405 406 thirdPartyTool.mergeLicenses(licenseMap, mergedLicense); 407 } 408 } 409 } 410 411 /** 412 * This returns the complete transitive dependency tree keyed by license type after applying some cleanup 413 */ 414 protected LicenseMap createLicenseMap(SortedMap<String, MavenProject> dependencies) { 415 LicenseMap licenseMap = new LicenseMap(); 416 for (MavenProject dependency : dependencies.values()) { 417 thirdPartyTool.addLicense(licenseMap, dependency, dependency.getLicenses()); 418 } 419 return licenseMap; 420 } 421 422 protected boolean checkUnsafeDependencies() { 423 SortedSet<MavenProject> unsafeDependencies = getUnsafeDependencies(); 424 boolean unsafe = !CollectionUtils.isEmpty(unsafeDependencies); 425 if (unsafe) { 426 Log log = getLog(); 427 log.debug("There are " + unsafeDependencies.size() + " dependencies with no license :"); 428 for (MavenProject dep : unsafeDependencies) { 429 430 // no license found for the dependency 431 log.debug(" - " + MojoHelper.getArtifactId(dep.getArtifact())); 432 } 433 } 434 return unsafe; 435 } 436 437 protected void writeThirdPartyFile() throws IOException { 438 439 Log log = getLog(); 440 LicenseMap licenseMap = getLicenseMap(); 441 File target = getThirdPartyFile(); 442 443 if (isDoGenerate()) { 444 StringBuilder sb = new StringBuilder(); 445 if (licenseMap.isEmpty()) { 446 sb.append(NO_DEPENDENCIES_MESSAGE); 447 } else { 448 if (isGroupByLicense()) { 449 450 // group by license 451 sb.append("List of third-party dependencies grouped by " + "their license type."); 452 for (String licenseName : licenseMap.keySet()) { 453 SortedSet<MavenProject> projects = licenseMap.get(licenseName); 454 455 // Don't print the license if it isn't being used 456 if (projects == null || projects.size() == 0) { 457 continue; 458 } 459 460 sb.append("\n\n").append(licenseName).append(" : "); 461 462 for (MavenProject mavenProject : projects) { 463 String s = MojoHelper.getArtifactName(mavenProject); 464 sb.append("\n * ").append(s); 465 } 466 } 467 468 } else { 469 470 // group by dependencies 471 SortedMap<MavenProject, String[]> map = licenseMap.toDependencyMap(); 472 473 sb.append("List of ").append(map.size()).append(" third-party dependencies.\n"); 474 475 List<String> lines = new ArrayList<String>(); 476 477 for (Map.Entry<MavenProject, String[]> entry : map.entrySet()) { 478 String artifact = MojoHelper.getArtifactName(entry.getKey()); 479 StringBuilder buffer = new StringBuilder(); 480 for (String license : entry.getValue()) { 481 buffer.append(" (").append(license).append(")"); 482 } 483 String licenses = buffer.toString(); 484 String line = licenses + " " + artifact; 485 lines.add(line); 486 } 487 488 Collections.sort(lines); 489 for (String line : lines) { 490 sb.append('\n').append(line); 491 } 492 lines.clear(); 493 } 494 } 495 String content = sb.toString(); 496 497 log.info("Writing third-party file to " + target); 498 if (isVerbose()) { 499 log.info(content); 500 } 501 502 FileUtil.writeString(target, content, getEncoding()); 503 } 504 505 if (isDoGenerateBundle()) { 506 507 // creates the bundled license file 508 File bundleTarget = FileUtil.getFile(getOutputDirectory(), getBundleThirdPartyPath()); 509 log.info("Writing bundled third-party file to " + bundleTarget); 510 FileUtil.copyFile(target, bundleTarget); 511 } 512 } 513 514 public boolean isGroupByLicense() { 515 return groupByLicense; 516 } 517 518 public void setGroupByLicense(boolean groupByLicense) { 519 this.groupByLicense = groupByLicense; 520 } 521 522 public File getOutputDirectory() { 523 return outputDirectory; 524 } 525 526 public String getThirdPartyFilename() { 527 return thirdPartyFilename; 528 } 529 530 public String getBundleThirdPartyPath() { 531 return bundleThirdPartyPath; 532 } 533 534 public boolean isGenerateBundle() { 535 return generateBundle; 536 } 537 538 public boolean isFailIfWarning() { 539 return failIfWarning; 540 } 541 542 public SortedMap<String, MavenProject> getProjectDependencies() { 543 return projectDependencies; 544 } 545 546 public SortedSet<MavenProject> getUnsafeDependencies() { 547 return unsafeDependencies; 548 } 549 550 public void setUnsafeDependencies(SortedSet<MavenProject> unsafeDependencies) { 551 this.unsafeDependencies = unsafeDependencies; 552 } 553 554 public File getThirdPartyFile() { 555 return thirdPartyFile; 556 } 557 558 public LicenseMap getLicenseMap() { 559 return licenseMap; 560 } 561 562 public void setOutputDirectory(File outputDirectory) { 563 this.outputDirectory = outputDirectory; 564 } 565 566 public void setThirdPartyFilename(String thirdPartyFilename) { 567 this.thirdPartyFilename = thirdPartyFilename; 568 } 569 570 public void setBundleThirdPartyPath(String bundleThirdPartyPath) { 571 this.bundleThirdPartyPath = bundleThirdPartyPath; 572 } 573 574 public void setGenerateBundle(boolean generateBundle) { 575 this.generateBundle = generateBundle; 576 } 577 578 public void setThirdPartyFile(File thirdPartyFile) { 579 this.thirdPartyFile = thirdPartyFile; 580 } 581 582 public boolean isUseMissingFile() { 583 return useMissingFile; 584 } 585 586 public File getMissingFile() { 587 return missingFile; 588 } 589 590 public void setUseMissingFile(boolean useMissingFile) { 591 this.useMissingFile = useMissingFile; 592 } 593 594 public void setMissingFile(File missingFile) { 595 this.missingFile = missingFile; 596 } 597 598 public void setFailIfWarning(boolean failIfWarning) { 599 this.failIfWarning = failIfWarning; 600 } 601 602 public SortedProperties getUnsafeMappings() { 603 return unsafeMappings; 604 } 605 606 public boolean isForce() { 607 return force; 608 } 609 610 public boolean isDoGenerate() { 611 return doGenerate; 612 } 613 614 public void setForce(boolean force) { 615 this.force = force; 616 } 617 618 public void setDoGenerate(boolean doGenerate) { 619 this.doGenerate = doGenerate; 620 } 621 622 public boolean isDoGenerateBundle() { 623 return doGenerateBundle; 624 } 625 626 public void setDoGenerateBundle(boolean doGenerateBundle) { 627 this.doGenerateBundle = doGenerateBundle; 628 } 629 630 public List<String> getExcludedScopes() { 631 String[] split = excludedScopes == null ? new String[0] : excludedScopes.split(","); 632 return Arrays.asList(split); 633 } 634 635 public void setExcludedScopes(String excludedScopes) { 636 this.excludedScopes = excludedScopes; 637 } 638 639 public List<String> getIncludedScopes() { 640 String[] split = includedScopes == null ? new String[0] : includedScopes.split(","); 641 return Arrays.asList(split); 642 } 643 644 public void setIncludedScopes(String includedScopes) { 645 this.includedScopes = includedScopes; 646 } 647 648 public String getExcludedGroups() { 649 return excludedGroups; 650 } 651 652 public void setExcludedGroups(String excludedGroups) { 653 this.excludedGroups = excludedGroups; 654 } 655 656 public String getIncludedGroups() { 657 return includedGroups; 658 } 659 660 public void setIncludedGroups(String includedGroups) { 661 this.includedGroups = includedGroups; 662 } 663 664 public String getExcludedArtifacts() { 665 return excludedArtifacts; 666 } 667 668 public void setExcludedArtifacts(String excludedArtifacts) { 669 this.excludedArtifacts = excludedArtifacts; 670 } 671 672 public String getIncludedArtifacts() { 673 return includedArtifacts; 674 } 675 676 public void setIncludedArtifacts(String includedArtifacts) { 677 this.includedArtifacts = includedArtifacts; 678 } 679 680 public ThirdPartyTool getThirdPartyTool() { 681 return thirdPartyTool; 682 } 683 684 public void setThirdPartyTool(ThirdPartyTool thridPartyTool) { 685 this.thirdPartyTool = thridPartyTool; 686 } 687 688 public String getArtifactLicenseMapping() { 689 return artifactLicenseMapping; 690 } 691 692 public void setArtifactLicenseMapping(String artifactLicenseMapping) { 693 this.artifactLicenseMapping = artifactLicenseMapping; 694 } 695 }