View Javadoc

1   /**
2    * Copyright 2010-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.codehaus.mojo.license;
17  
18  import java.io.File;
19  import java.io.FileInputStream;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.OutputStream;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.SortedMap;
30  import java.util.SortedSet;
31  import java.util.TreeMap;
32  
33  import org.apache.commons.collections.CollectionUtils;
34  import org.apache.commons.io.FileUtils;
35  import org.apache.commons.io.IOUtils;
36  import org.apache.commons.lang.StringUtils;
37  import org.apache.maven.plugin.MojoFailureException;
38  import org.apache.maven.plugin.logging.Log;
39  import org.apache.maven.project.MavenProject;
40  import org.apache.maven.project.ProjectBuildingException;
41  import org.codehaus.mojo.license.model.LicenseMap;
42  import org.springframework.core.io.DefaultResourceLoader;
43  import org.springframework.core.io.Resource;
44  import org.springframework.core.io.ResourceLoader;
45  
46  /**
47   * Abstract mojo for all third-party mojos.
48   *
49   * @author tchemit <chemit@codelutin.com>
50   * @since 1.0
51   */
52  public abstract class AbstractAddThirdPartyMojo extends AbstractLicenseMojo {
53  
54      /**
55       * Directory where to generate files.
56       *
57       * @parameter expression="${license.outputDirectory}"
58       *            default-value="${project.build.directory}/generated-sources/license"
59       * @required
60       * @since 1.0
61       */
62      protected File outputDirectory;
63  
64      /**
65       * File where license information for third party dependencies gets stored
66       *
67       * @parameter expression="${license.thirdPartyFilename}" default-value="THIRD-PARTY.txt"
68       * @required
69       * @since 1.0
70       */
71      protected String thirdPartyFilename;
72  
73      /**
74       * A flag to use the missing licenses file to consolidate the THID-PARTY file.
75       *
76       * @parameter expression="${license.useMissingFile}" default-value="false"
77       * @since 1.0
78       */
79      protected boolean useMissingFile;
80  
81      /**
82       * The file where to fill the license for dependencies with unknown license.
83       *
84       * @parameter expression="${license.missingFile}" default-value="src/license/THIRD-PARTY.properties"
85       * @since 1.0
86       */
87      protected File missingFile;
88  
89      /**
90       * Location of a properties file mapping artifacts without a license to the license that should be used for them.
91       * This supports classpath notation and any other type of URL Spring 3.1 resource loading can understand. This
92       * properties file supports matching by groupId, groupId + artifactId, or groupId+artifactId+version.
93       *
94       * @parameter expression="${license.artifactLicenseMapping}" default-value="classpath:THIRD-PARTY.properties"
95       * @since 1.0
96       */
97      protected String artifactLicenseMapping;
98  
99      /**
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      * &lt;licenseMerges&gt;
109      * &lt;licenseMerge&gt;The Apache Software License|Version 2.0,Apache License, Version 2.0&lt;/licenseMerge&gt;
110      * &lt;/licenseMerges&gt;
111      * &lt;/pre&gt;
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 }