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.IOException;
020    import java.io.InputStream;
021    import java.io.OutputStream;
022    import java.util.ArrayList;
023    import java.util.Arrays;
024    import java.util.Collections;
025    import java.util.HashMap;
026    import java.util.List;
027    import java.util.Map;
028    import java.util.SortedMap;
029    import java.util.SortedSet;
030    import java.util.TreeMap;
031    
032    import org.apache.commons.collections.CollectionUtils;
033    import org.apache.commons.io.FileUtils;
034    import org.apache.commons.io.IOUtils;
035    import org.apache.commons.lang.StringUtils;
036    import org.apache.maven.plugin.MojoFailureException;
037    import org.apache.maven.plugin.logging.Log;
038    import org.apache.maven.project.MavenProject;
039    import org.apache.maven.project.ProjectBuildingException;
040    import org.codehaus.mojo.license.model.LicenseMap;
041    import org.springframework.core.io.DefaultResourceLoader;
042    import org.springframework.core.io.Resource;
043    import org.springframework.core.io.ResourceLoader;
044    
045    /**
046     * Abstract mojo for all third-party mojos.
047     *
048     * @author tchemit <chemit@codelutin.com>
049     * @since 1.0
050     */
051    public abstract class AbstractAddThirdPartyMojo extends AbstractLicenseMojo {
052    
053        /**
054         * Directory where to generate files.
055         *
056         * @parameter expression="${license.outputDirectory}"
057         *            default-value="${project.build.directory}/generated-sources/license"
058         * @required
059         * @since 1.0
060         */
061        protected File outputDirectory;
062    
063        /**
064         * File where to wirte the third-party file.
065         *
066         * @parameter expression="${license.thirdPartyFilename}" default-value="THIRD-PARTY.txt"
067         * @required
068         * @since 1.0
069         */
070        protected String thirdPartyFilename;
071    
072        /**
073         * A flag to use the missing licenses file to consolidate the THID-PARTY file.
074         *
075         * @parameter expression="${license.useMissingFile}" default-value="false"
076         * @since 1.0
077         */
078        protected boolean useMissingFile;
079    
080        /**
081         * The file where to fill the license for dependencies with unknown license.
082         *
083         * @parameter expression="${license.missingFile}" default-value="src/license/THIRD-PARTY.properties"
084         * @since 1.0
085         */
086        protected File missingFile;
087    
088        /**
089         * Location of a properties file mapping artifacts that are published with no license to the license that should be
090         * used for them. This supports classpath notation and any other type of URL Spring 3.1 resource loading can
091         * understand.
092         *
093         * @parameter expression="${license.artifactLicenseMapping}" default-value="THIRD-PARTY.properties"
094         * @since 1.0
095         */
096        protected String artifactLicenseMapping;
097    
098        /**
099         * To merge licenses in final file.
100         * <p/>
101         * Each entry represents a merge (first license is main license to keep), licenses are separated by {@code |}.
102         * <p/>
103         * Example :
104         * <p/>
105         *
106         * <pre>
107         * &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 dependecies.
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 thridPartyTool;
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            ResourceLoader loader = new DefaultResourceLoader();
267            Resource resource = loader.getResource(location);
268            return resource.exists();
269        }
270    
271        protected File copyToFileSystem(String location) {
272            ResourceLoader loader = new DefaultResourceLoader();
273            Resource resource = loader.getResource(location);
274            if (!resource.exists()) {
275                throw new IllegalArgumentException("Can't locate " + location);
276            }
277    
278            InputStream in = null;
279            OutputStream out = null;
280            try {
281                in = resource.getInputStream();
282                File temp = new File(getProject().getBuild().getDirectory() + "/license/THIRD-PARTY.properties");
283                out = FileUtils.openOutputStream(temp);
284                IOUtils.copy(in, out);
285                getLog().debug("Created " + temp);
286                return temp;
287            } catch (IOException e) {
288                throw new IllegalArgumentException(e);
289            } finally {
290                IOUtils.closeQuietly(in);
291                IOUtils.closeQuietly(out);
292            }
293        }
294    
295        @Override
296        protected void init() throws Exception {
297            if (exists(getArtifactLicenseMapping())) {
298                File propertiesFile = copyToFileSystem(getArtifactLicenseMapping());
299                setMissingFile(propertiesFile);
300            }
301    
302            Log log = getLog();
303    
304            if (log.isDebugEnabled()) {
305    
306                // always be verbose in debug mode
307                setVerbose(true);
308            }
309    
310            File file = new File(getOutputDirectory(), getThirdPartyFilename());
311    
312            setThirdPartyFile(file);
313    
314            long buildTimestamp = getBuildTimestamp();
315    
316            if (isVerbose()) {
317                log.info("Build start   at : " + buildTimestamp);
318                log.info("third-party file : " + file.lastModified());
319            }
320    
321            setDoGenerate(isForce() || !file.exists() || buildTimestamp > file.lastModified());
322    
323            if (isGenerateBundle()) {
324    
325                File bundleFile = FileUtil.getFile(getOutputDirectory(), getBundleThirdPartyPath());
326    
327                if (isVerbose()) {
328                    log.info("bundle third-party file : " + bundleFile.lastModified());
329                }
330                setDoGenerateBundle(isForce() || !bundleFile.exists() || buildTimestamp > bundleFile.lastModified());
331            } else {
332    
333                // not generating bundled file
334                setDoGenerateBundle(false);
335            }
336    
337            projectDependencies = loadDependencies();
338    
339            licenseMap = createLicenseMap(projectDependencies);
340    
341            SortedSet<MavenProject> unsafeDependencies = getThridPartyTool().getProjectsWithNoLicense(licenseMap,
342                    isVerbose());
343    
344            setUnsafeDependencies(unsafeDependencies);
345    
346            if (!CollectionUtils.isEmpty(unsafeDependencies) && isUseMissingFile() && isDoGenerate()) {
347    
348                // load unsafeMapping
349                unsafeMappings = createUnsafeMapping();
350            }
351    
352            if (!CollectionUtils.isEmpty(licenseMerges)) {
353    
354                // check where is not multi licenses merged main licenses (see OJO-1723)
355                Map<String, String[]> mergedLicenses = new HashMap<String, String[]>();
356    
357                for (String merge : licenseMerges) {
358                    merge = merge.trim();
359                    String[] split = merge.split("\\|");
360    
361                    String mainLicense = split[0];
362    
363                    if (mergedLicenses.containsKey(mainLicense)) {
364    
365                        // this license was already describe, fail the build...
366    
367                        throw new MojoFailureException(
368                                "The merge main license "
369                                        + mainLicense
370                                        + " was already registred in the "
371                                        + "configuration, please use only one such entry as describe in example "
372                                        + "http://mojo.codehaus.org/license-maven-plugin/examples/example-thirdparty.html#Merge_licenses.");
373                    }
374                    mergedLicenses.put(mainLicense, split);
375                }
376    
377                // merge licenses in license map
378    
379                for (String[] mergedLicense : mergedLicenses.values()) {
380                    if (isVerbose()) {
381                        getLog().info("Will merge " + Arrays.toString(mergedLicense) + "");
382                    }
383    
384                    thridPartyTool.mergeLicenses(licenseMap, mergedLicense);
385                }
386            }
387        }
388    
389        protected LicenseMap createLicenseMap(SortedMap<String, MavenProject> dependencies) {
390    
391            LicenseMap licenseMap = new LicenseMap();
392    
393            for (MavenProject project : dependencies.values()) {
394                thridPartyTool.addLicense(licenseMap, project, project.getLicenses());
395            }
396            return licenseMap;
397        }
398    
399        protected boolean checkUnsafeDependencies() {
400            SortedSet<MavenProject> unsafeDependencies = getUnsafeDependencies();
401            boolean unsafe = !CollectionUtils.isEmpty(unsafeDependencies);
402            if (unsafe) {
403                Log log = getLog();
404                log.debug("There is " + unsafeDependencies.size() + " dependencies with no license :");
405                for (MavenProject dep : unsafeDependencies) {
406    
407                    // no license found for the dependency
408                    log.info(" - " + MojoHelper.getArtifactId(dep.getArtifact()));
409                }
410            }
411            return unsafe;
412        }
413    
414        protected void writeThirdPartyFile() throws IOException {
415    
416            Log log = getLog();
417            LicenseMap licenseMap = getLicenseMap();
418            File target = getThirdPartyFile();
419    
420            if (isDoGenerate()) {
421                StringBuilder sb = new StringBuilder();
422                if (licenseMap.isEmpty()) {
423                    sb.append(NO_DEPENDENCIES_MESSAGE);
424                } else {
425                    if (isGroupByLicense()) {
426    
427                        // group by license
428                        sb.append("List of third-party dependencies grouped by " + "their license type.");
429                        for (String licenseName : licenseMap.keySet()) {
430                            SortedSet<MavenProject> projects = licenseMap.get(licenseName);
431                            sb.append("\n\n").append(licenseName).append(" : ");
432    
433                            for (MavenProject mavenProject : projects) {
434                                String s = MojoHelper.getArtifactName(mavenProject);
435                                sb.append("\n  * ").append(s);
436                            }
437                        }
438    
439                    } else {
440    
441                        // group by dependencies
442                        SortedMap<MavenProject, String[]> map = licenseMap.toDependencyMap();
443    
444                        sb.append("List of ").append(map.size()).append(" third-party dependencies.\n");
445    
446                        List<String> lines = new ArrayList<String>();
447    
448                        for (Map.Entry<MavenProject, String[]> entry : map.entrySet()) {
449                            String artifact = MojoHelper.getArtifactName(entry.getKey());
450                            StringBuilder buffer = new StringBuilder();
451                            for (String license : entry.getValue()) {
452                                buffer.append(" (").append(license).append(")");
453                            }
454                            String licenses = buffer.toString();
455                            String line = licenses + " " + artifact;
456                            lines.add(line);
457                        }
458    
459                        Collections.sort(lines);
460                        for (String line : lines) {
461                            sb.append('\n').append(line);
462                        }
463                        lines.clear();
464                    }
465                }
466                String content = sb.toString();
467    
468                log.info("Writing third-party file to " + target);
469                if (isVerbose()) {
470                    log.info(content);
471                }
472    
473                FileUtil.writeString(target, content, getEncoding());
474            }
475    
476            if (isDoGenerateBundle()) {
477    
478                // creates the bundled license file
479                File bundleTarget = FileUtil.getFile(getOutputDirectory(), getBundleThirdPartyPath());
480                log.info("Writing bundled third-party file to " + bundleTarget);
481                FileUtil.copyFile(target, bundleTarget);
482            }
483        }
484    
485        public boolean isGroupByLicense() {
486            return groupByLicense;
487        }
488    
489        public void setGroupByLicense(boolean groupByLicense) {
490            this.groupByLicense = groupByLicense;
491        }
492    
493        public File getOutputDirectory() {
494            return outputDirectory;
495        }
496    
497        public String getThirdPartyFilename() {
498            return thirdPartyFilename;
499        }
500    
501        public String getBundleThirdPartyPath() {
502            return bundleThirdPartyPath;
503        }
504    
505        public boolean isGenerateBundle() {
506            return generateBundle;
507        }
508    
509        public boolean isFailIfWarning() {
510            return failIfWarning;
511        }
512    
513        public SortedMap<String, MavenProject> getProjectDependencies() {
514            return projectDependencies;
515        }
516    
517        public SortedSet<MavenProject> getUnsafeDependencies() {
518            return unsafeDependencies;
519        }
520    
521        public void setUnsafeDependencies(SortedSet<MavenProject> unsafeDependencies) {
522            this.unsafeDependencies = unsafeDependencies;
523        }
524    
525        public File getThirdPartyFile() {
526            return thirdPartyFile;
527        }
528    
529        public LicenseMap getLicenseMap() {
530            return licenseMap;
531        }
532    
533        public void setOutputDirectory(File outputDirectory) {
534            this.outputDirectory = outputDirectory;
535        }
536    
537        public void setThirdPartyFilename(String thirdPartyFilename) {
538            this.thirdPartyFilename = thirdPartyFilename;
539        }
540    
541        public void setBundleThirdPartyPath(String bundleThirdPartyPath) {
542            this.bundleThirdPartyPath = bundleThirdPartyPath;
543        }
544    
545        public void setGenerateBundle(boolean generateBundle) {
546            this.generateBundle = generateBundle;
547        }
548    
549        public void setThirdPartyFile(File thirdPartyFile) {
550            this.thirdPartyFile = thirdPartyFile;
551        }
552    
553        public boolean isUseMissingFile() {
554            return useMissingFile;
555        }
556    
557        public File getMissingFile() {
558            return missingFile;
559        }
560    
561        public void setUseMissingFile(boolean useMissingFile) {
562            this.useMissingFile = useMissingFile;
563        }
564    
565        public void setMissingFile(File missingFile) {
566            this.missingFile = missingFile;
567        }
568    
569        public void setFailIfWarning(boolean failIfWarning) {
570            this.failIfWarning = failIfWarning;
571        }
572    
573        public SortedProperties getUnsafeMappings() {
574            return unsafeMappings;
575        }
576    
577        public boolean isForce() {
578            return force;
579        }
580    
581        public boolean isDoGenerate() {
582            return doGenerate;
583        }
584    
585        public void setForce(boolean force) {
586            this.force = force;
587        }
588    
589        public void setDoGenerate(boolean doGenerate) {
590            this.doGenerate = doGenerate;
591        }
592    
593        public boolean isDoGenerateBundle() {
594            return doGenerateBundle;
595        }
596    
597        public void setDoGenerateBundle(boolean doGenerateBundle) {
598            this.doGenerateBundle = doGenerateBundle;
599        }
600    
601        public List<String> getExcludedScopes() {
602            String[] split = excludedScopes == null ? new String[0] : excludedScopes.split(",");
603            return Arrays.asList(split);
604        }
605    
606        public void setExcludedScopes(String excludedScopes) {
607            this.excludedScopes = excludedScopes;
608        }
609    
610        public List<String> getIncludedScopes() {
611            String[] split = includedScopes == null ? new String[0] : includedScopes.split(",");
612            return Arrays.asList(split);
613        }
614    
615        public void setIncludedScopes(String includedScopes) {
616            this.includedScopes = includedScopes;
617        }
618    
619        public String getExcludedGroups() {
620            return excludedGroups;
621        }
622    
623        public void setExcludedGroups(String excludedGroups) {
624            this.excludedGroups = excludedGroups;
625        }
626    
627        public String getIncludedGroups() {
628            return includedGroups;
629        }
630    
631        public void setIncludedGroups(String includedGroups) {
632            this.includedGroups = includedGroups;
633        }
634    
635        public String getExcludedArtifacts() {
636            return excludedArtifacts;
637        }
638    
639        public void setExcludedArtifacts(String excludedArtifacts) {
640            this.excludedArtifacts = excludedArtifacts;
641        }
642    
643        public String getIncludedArtifacts() {
644            return includedArtifacts;
645        }
646    
647        public void setIncludedArtifacts(String includedArtifacts) {
648            this.includedArtifacts = includedArtifacts;
649        }
650    
651        public ThirdPartyTool getThridPartyTool() {
652            return thridPartyTool;
653        }
654    
655        public void setThridPartyTool(ThirdPartyTool thridPartyTool) {
656            this.thridPartyTool = thridPartyTool;
657        }
658    
659        public String getArtifactLicenseMapping() {
660            return artifactLicenseMapping;
661        }
662    
663        public void setArtifactLicenseMapping(String artifactLicenseMapping) {
664            this.artifactLicenseMapping = artifactLicenseMapping;
665        }
666    }