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 }