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