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.
92       *
93       * @parameter expression="${license.artifactLicenseMapping}" default-value="classpath:THIRD-PARTY.properties"
94       * @since 1.0
95       */
96      protected String artifactLicenseMapping;
97  
98      /**
99       * 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      * &lt;licenseMerges&gt;
108      * &lt;licenseMerge&gt;The Apache Software License|Version 2.0,Apache License, Version 2.0&lt;/licenseMerge&gt;
109      * &lt;/licenseMerges&gt;
110      * &lt;/pre&gt;
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 }