001    /*
002     * #%L
003     * License Maven Plugin
004     *
005     * $Id: AbstractAddThirdPartyMojo.java 14605 2011-08-21 01:11:25Z tchemit $
006     * $HeadURL: http://svn.codehaus.org/mojo/tags/license-maven-plugin-1.0/src/main/java/org/codehaus/mojo/license/AbstractAddThirdPartyMojo.java $
007     * %%
008     * Copyright (C) 2008 - 2011 CodeLutin, Codehaus, Tony Chemit
009     * %%
010     * This program is free software: you can redistribute it and/or modify
011     * it under the terms of the GNU Lesser General Public License as
012     * published by the Free Software Foundation, either version 3 of the
013     * License, or (at your option) any later version.
014     *
015     * This program is distributed in the hope that it will be useful,
016     * but WITHOUT ANY WARRANTY; without even the implied warranty of
017     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
018     * GNU General Lesser Public License for more details.
019     *
020     * You should have received a copy of the GNU General Lesser Public
021     * License along with this program.  If not, see
022     * <http://www.gnu.org/licenses/lgpl-3.0.html>.
023     * #L%
024     */
025    
026    package org.codehaus.mojo.license;
027    
028    import java.io.File;
029    import java.io.IOException;
030    import java.io.InputStream;
031    import java.io.OutputStream;
032    import java.util.ArrayList;
033    import java.util.Arrays;
034    import java.util.Collections;
035    import java.util.HashMap;
036    import java.util.List;
037    import java.util.Map;
038    import java.util.SortedMap;
039    import java.util.SortedSet;
040    import java.util.TreeMap;
041    
042    import org.apache.commons.collections.CollectionUtils;
043    import org.apache.commons.io.FileUtils;
044    import org.apache.commons.io.IOUtils;
045    import org.apache.commons.lang.StringUtils;
046    import org.apache.maven.plugin.MojoFailureException;
047    import org.apache.maven.plugin.logging.Log;
048    import org.apache.maven.project.MavenProject;
049    import org.apache.maven.project.ProjectBuildingException;
050    import org.codehaus.mojo.license.model.LicenseMap;
051    import org.springframework.core.io.DefaultResourceLoader;
052    import org.springframework.core.io.Resource;
053    import org.springframework.core.io.ResourceLoader;
054    
055    /**
056     * Abstract mojo for all third-party mojos.
057     *
058     * @author tchemit <chemit@codelutin.com>
059     * @since 1.0
060     */
061    public abstract class AbstractAddThirdPartyMojo extends AbstractLicenseMojo {
062    
063        /**
064         * Directory where to generate files.
065         *
066         * @parameter expression="${license.outputDirectory}"
067         *            default-value="${project.build.directory}/generated-sources/license"
068         * @required
069         * @since 1.0
070         */
071        protected File outputDirectory;
072    
073        /**
074         * File where to wirte the third-party file.
075         *
076         * @parameter expression="${license.thirdPartyFilename}" default-value="THIRD-PARTY.txt"
077         * @required
078         * @since 1.0
079         */
080        protected String thirdPartyFilename;
081    
082        /**
083         * A flag to use the missing licenses file to consolidate the THID-PARTY file.
084         *
085         * @parameter expression="${license.useMissingFile}" default-value="false"
086         * @since 1.0
087         */
088        protected boolean useMissingFile;
089    
090        /**
091         * The file where to fill the license for dependencies with unknown license.
092         *
093         * @parameter expression="${license.missingFile}" default-value="src/license/THIRD-PARTY.properties"
094         * @since 1.0
095         */
096        protected File missingFile;
097    
098        /**
099         * Location of a properties file mapping artifacts that are published with no license to the license that should be
100         * used for them. This supports classpath notation and any other type of URL Spring 3.1 resource loading can
101         * understand.
102         *
103         * @parameter expression="${license.artifactLicenseMapping}" default-value="THIRD-PARTY.properties"
104         * @since 1.0
105         */
106        protected String artifactLicenseMapping;
107    
108        /**
109         * To merge licenses in final file.
110         * <p/>
111         * Each entry represents a merge (first license is main license to keep), licenses are separated by {@code |}.
112         * <p/>
113         * Example :
114         * <p/>
115         *
116         * <pre>
117         * &lt;licenseMerges&gt;
118         * &lt;licenseMerge&gt;The Apache Software License|Version 2.0,Apache License, Version 2.0&lt;/licenseMerge&gt;
119         * &lt;/licenseMerges&gt;
120         * &lt;/pre&gt;
121         *
122         * @parameter
123         * @since 1.0
124         */
125        protected List<String> licenseMerges;
126    
127        /**
128         * The path of the bundled third party file to produce when {@link #generateBundle} is on.
129         * <p/>
130         * <b>Note:</b> This option is not available for {@code pom} module types.
131         *
132         * @parameter expression="${license.bundleThirdPartyPath}"
133         *            default-value="META-INF/${project.artifactId}-THIRD-PARTY.txt"
134         * @since 1.0
135         */
136        protected String bundleThirdPartyPath;
137    
138        /**
139         * A flag to copy a bundled version of the third-party file. This is usefull to avoid for a final application
140         * collision name of third party file.
141         * <p/>
142         * The file will be copied at the {@link #bundleThirdPartyPath} location.
143         *
144         * @parameter expression="${license.generateBundle}" default-value="false"
145         * @since 1.0
146         */
147        protected boolean generateBundle;
148    
149        /**
150         * To force generation of the third-party file even if every thing is up to date.
151         *
152         * @parameter expression="${license.force}" default-value="false"
153         * @since 1.0
154         */
155        protected boolean force;
156    
157        /**
158         * A flag to fail the build if at least one dependency was detected without a license.
159         *
160         * @parameter expression="${license.failIfWarning}" default-value="false"
161         * @since 1.0
162         */
163        protected boolean failIfWarning;
164    
165        /**
166         * A flag to change the grouping of the generated THIRD-PARTY file.
167         * <p/>
168         * By default, group by dependecies.
169         * <p/>
170         * If sets to {@code true}, the it will group by license type.
171         *
172         * @parameter expression="${license.groupByLicense}" default-value="false"
173         * @since 1.0
174         */
175        protected boolean groupByLicense;
176    
177        /**
178         * A filter to exclude some scopes.
179         *
180         * @parameter expression="${license.excludedScopes}" default-value="system"
181         * @since 1.0
182         */
183        protected String excludedScopes;
184    
185        /**
186         * A filter to include only some scopes, if let empty then all scopes will be used (no filter).
187         *
188         * @parameter expression="${license.includedScopes}" default-value=""
189         * @since 1.0
190         */
191        protected String includedScopes;
192    
193        /**
194         * A filter to exclude some GroupIds
195         *
196         * @parameter expression="${license.excludedGroups}" default-value=""
197         * @since 1.0
198         */
199        protected String excludedGroups;
200    
201        /**
202         * A filter to include only some GroupIds
203         *
204         * @parameter expression="${license.includedGroups}" default-value=""
205         * @since 1.0
206         */
207        protected String includedGroups;
208    
209        /**
210         * A filter to exclude some ArtifactsIds
211         *
212         * @parameter expression="${license.excludedArtifacts}" default-value=""
213         * @since 1.0
214         */
215        protected String excludedArtifacts;
216    
217        /**
218         * A filter to include only some ArtifactsIds
219         *
220         * @parameter expression="${license.includedArtifacts}" default-value=""
221         * @since 1.0
222         */
223        protected String includedArtifacts;
224    
225        /**
226         * Include transitive dependencies when downloading license files.
227         *
228         * @parameter default-value="true"
229         * @since 1.0
230         */
231        protected boolean includeTransitiveDependencies;
232    
233        /**
234         * third party tool.
235         *
236         * @component
237         * @readonly
238         * @since 1.0
239         */
240        private ThirdPartyTool thridPartyTool;
241    
242        private SortedMap<String, MavenProject> projectDependencies;
243    
244        private LicenseMap licenseMap;
245    
246        private SortedSet<MavenProject> unsafeDependencies;
247    
248        private File thirdPartyFile;
249    
250        private SortedProperties unsafeMappings;
251    
252        private boolean doGenerate;
253    
254        private boolean doGenerateBundle;
255    
256        public static final String NO_DEPENDENCIES_MESSAGE = "the project has no dependencies.";
257    
258        private static SortedMap<String, MavenProject> artifactCache;
259    
260        public static SortedMap<String, MavenProject> getArtifactCache() {
261            if (artifactCache == null) {
262                artifactCache = new TreeMap<String, MavenProject>();
263            }
264            return artifactCache;
265        }
266    
267        protected abstract SortedMap<String, MavenProject> loadDependencies();
268    
269        protected abstract SortedProperties createUnsafeMapping() throws ProjectBuildingException, IOException,
270                ThirdPartyToolException;
271    
272        protected boolean exists(String location) {
273            if (StringUtils.isBlank(location)) {
274                return false;
275            }
276            ResourceLoader loader = new DefaultResourceLoader();
277            Resource resource = loader.getResource(location);
278            return resource.exists();
279        }
280    
281        protected File copyToFileSystem(String location) {
282            ResourceLoader loader = new DefaultResourceLoader();
283            Resource resource = loader.getResource(location);
284            if (!resource.exists()) {
285                throw new IllegalArgumentException("Can't locate " + location);
286            }
287    
288            InputStream in = null;
289            OutputStream out = null;
290            try {
291                in = resource.getInputStream();
292                File temp = new File(getProject().getBuild().getDirectory() + "/license/THIRD-PARTY.properties");
293                out = FileUtils.openOutputStream(temp);
294                IOUtils.copy(in, out);
295                getLog().debug("Created " + temp);
296                return temp;
297            } catch (IOException e) {
298                throw new IllegalArgumentException(e);
299            } finally {
300                IOUtils.closeQuietly(in);
301                IOUtils.closeQuietly(out);
302            }
303        }
304    
305        @Override
306        protected void init() throws Exception {
307            if (exists(getArtifactLicenseMapping())) {
308                File propertiesFile = copyToFileSystem(getArtifactLicenseMapping());
309                setMissingFile(propertiesFile);
310            }
311    
312    
313            Log log = getLog();
314    
315            if (log.isDebugEnabled()) {
316    
317                // always be verbose in debug mode
318                setVerbose(true);
319            }
320    
321            File file = new File(getOutputDirectory(), getThirdPartyFilename());
322    
323            setThirdPartyFile(file);
324    
325            long buildTimestamp = getBuildTimestamp();
326    
327            if (isVerbose()) {
328                log.info("Build start   at : " + buildTimestamp);
329                log.info("third-party file : " + file.lastModified());
330            }
331    
332            setDoGenerate(isForce() || !file.exists() || buildTimestamp > file.lastModified());
333    
334            if (isGenerateBundle()) {
335    
336                File bundleFile = FileUtil.getFile(getOutputDirectory(), getBundleThirdPartyPath());
337    
338                if (isVerbose()) {
339                    log.info("bundle third-party file : " + bundleFile.lastModified());
340                }
341                setDoGenerateBundle(isForce() || !bundleFile.exists() || buildTimestamp > bundleFile.lastModified());
342            } else {
343    
344                // not generating bundled file
345                setDoGenerateBundle(false);
346            }
347    
348            projectDependencies = loadDependencies();
349    
350            licenseMap = createLicenseMap(projectDependencies);
351    
352            SortedSet<MavenProject> unsafeDependencies = getThridPartyTool().getProjectsWithNoLicense(licenseMap,
353                    isVerbose());
354    
355            setUnsafeDependencies(unsafeDependencies);
356    
357            if (!CollectionUtils.isEmpty(unsafeDependencies) && isUseMissingFile() && isDoGenerate()) {
358    
359                // load unsafeMapping
360                unsafeMappings = createUnsafeMapping();
361            }
362    
363            if (!CollectionUtils.isEmpty(licenseMerges)) {
364    
365                // check where is not multi licenses merged main licenses (see OJO-1723)
366                Map<String, String[]> mergedLicenses = new HashMap<String, String[]>();
367    
368                for (String merge : licenseMerges) {
369                    merge = merge.trim();
370                    String[] split = merge.split("\\|");
371    
372                    String mainLicense = split[0];
373    
374                    if (mergedLicenses.containsKey(mainLicense)) {
375    
376                        // this license was already describe, fail the build...
377    
378                        throw new MojoFailureException(
379                                "The merge main license "
380                                        + mainLicense
381                                        + " was already registred in the "
382                                        + "configuration, please use only one such entry as describe in example "
383                                        + "http://mojo.codehaus.org/license-maven-plugin/examples/example-thirdparty.html#Merge_licenses.");
384                    }
385                    mergedLicenses.put(mainLicense, split);
386                }
387    
388                // merge licenses in license map
389    
390                for (String[] mergedLicense : mergedLicenses.values()) {
391                    if (isVerbose()) {
392                        getLog().info("Will merge " + Arrays.toString(mergedLicense) + "");
393                    }
394    
395                    thridPartyTool.mergeLicenses(licenseMap, mergedLicense);
396                }
397            }
398        }
399    
400        protected LicenseMap createLicenseMap(SortedMap<String, MavenProject> dependencies) {
401    
402            LicenseMap licenseMap = new LicenseMap();
403    
404            for (MavenProject project : dependencies.values()) {
405                thridPartyTool.addLicense(licenseMap, project, project.getLicenses());
406            }
407            return licenseMap;
408        }
409    
410        protected boolean checkUnsafeDependencies() {
411            SortedSet<MavenProject> unsafeDependencies = getUnsafeDependencies();
412            boolean unsafe = !CollectionUtils.isEmpty(unsafeDependencies);
413            if (unsafe) {
414                Log log = getLog();
415                log.debug("There is " + unsafeDependencies.size() + " dependencies with no license :");
416                for (MavenProject dep : unsafeDependencies) {
417    
418                    // no license found for the dependency
419                    log.info(" - " + MojoHelper.getArtifactId(dep.getArtifact()));
420                }
421            }
422            return unsafe;
423        }
424    
425        protected void writeThirdPartyFile() throws IOException {
426    
427            Log log = getLog();
428            LicenseMap licenseMap = getLicenseMap();
429            File target = getThirdPartyFile();
430    
431            if (isDoGenerate()) {
432                StringBuilder sb = new StringBuilder();
433                if (licenseMap.isEmpty()) {
434                    sb.append(NO_DEPENDENCIES_MESSAGE);
435                } else {
436                    if (isGroupByLicense()) {
437    
438                        // group by license
439                        sb.append("List of third-party dependencies grouped by " + "their license type.");
440                        for (String licenseName : licenseMap.keySet()) {
441                            SortedSet<MavenProject> projects = licenseMap.get(licenseName);
442                            sb.append("\n\n").append(licenseName).append(" : ");
443    
444                            for (MavenProject mavenProject : projects) {
445                                String s = MojoHelper.getArtifactName(mavenProject);
446                                sb.append("\n  * ").append(s);
447                            }
448                        }
449    
450                    } else {
451    
452                        // group by dependencies
453                        SortedMap<MavenProject, String[]> map = licenseMap.toDependencyMap();
454    
455                        sb.append("List of ").append(map.size()).append(" third-party dependencies.\n");
456    
457                        List<String> lines = new ArrayList<String>();
458    
459                        for (Map.Entry<MavenProject, String[]> entry : map.entrySet()) {
460                            String artifact = MojoHelper.getArtifactName(entry.getKey());
461                            StringBuilder buffer = new StringBuilder();
462                            for (String license : entry.getValue()) {
463                                buffer.append(" (").append(license).append(")");
464                            }
465                            String licenses = buffer.toString();
466                            String line = licenses + " " + artifact;
467                            lines.add(line);
468                        }
469    
470                        Collections.sort(lines);
471                        for (String line : lines) {
472                            sb.append('\n').append(line);
473                        }
474                        lines.clear();
475                    }
476                }
477                String content = sb.toString();
478    
479                log.info("Writing third-party file to " + target);
480                if (isVerbose()) {
481                    log.info(content);
482                }
483    
484                FileUtil.writeString(target, content, getEncoding());
485            }
486    
487            if (isDoGenerateBundle()) {
488    
489                // creates the bundled license file
490                File bundleTarget = FileUtil.getFile(getOutputDirectory(), getBundleThirdPartyPath());
491                log.info("Writing bundled third-party file to " + bundleTarget);
492                FileUtil.copyFile(target, bundleTarget);
493            }
494        }
495    
496        public boolean isGroupByLicense() {
497            return groupByLicense;
498        }
499    
500        public void setGroupByLicense(boolean groupByLicense) {
501            this.groupByLicense = groupByLicense;
502        }
503    
504        public File getOutputDirectory() {
505            return outputDirectory;
506        }
507    
508        public String getThirdPartyFilename() {
509            return thirdPartyFilename;
510        }
511    
512        public String getBundleThirdPartyPath() {
513            return bundleThirdPartyPath;
514        }
515    
516        public boolean isGenerateBundle() {
517            return generateBundle;
518        }
519    
520        public boolean isFailIfWarning() {
521            return failIfWarning;
522        }
523    
524        public SortedMap<String, MavenProject> getProjectDependencies() {
525            return projectDependencies;
526        }
527    
528        public SortedSet<MavenProject> getUnsafeDependencies() {
529            return unsafeDependencies;
530        }
531    
532        public void setUnsafeDependencies(SortedSet<MavenProject> unsafeDependencies) {
533            this.unsafeDependencies = unsafeDependencies;
534        }
535    
536        public File getThirdPartyFile() {
537            return thirdPartyFile;
538        }
539    
540        public LicenseMap getLicenseMap() {
541            return licenseMap;
542        }
543    
544        public void setOutputDirectory(File outputDirectory) {
545            this.outputDirectory = outputDirectory;
546        }
547    
548        public void setThirdPartyFilename(String thirdPartyFilename) {
549            this.thirdPartyFilename = thirdPartyFilename;
550        }
551    
552        public void setBundleThirdPartyPath(String bundleThirdPartyPath) {
553            this.bundleThirdPartyPath = bundleThirdPartyPath;
554        }
555    
556        public void setGenerateBundle(boolean generateBundle) {
557            this.generateBundle = generateBundle;
558        }
559    
560        public void setThirdPartyFile(File thirdPartyFile) {
561            this.thirdPartyFile = thirdPartyFile;
562        }
563    
564        public boolean isUseMissingFile() {
565            return useMissingFile;
566        }
567    
568        public File getMissingFile() {
569            return missingFile;
570        }
571    
572        public void setUseMissingFile(boolean useMissingFile) {
573            this.useMissingFile = useMissingFile;
574        }
575    
576        public void setMissingFile(File missingFile) {
577            this.missingFile = missingFile;
578        }
579    
580        public void setFailIfWarning(boolean failIfWarning) {
581            this.failIfWarning = failIfWarning;
582        }
583    
584        public SortedProperties getUnsafeMappings() {
585            return unsafeMappings;
586        }
587    
588        public boolean isForce() {
589            return force;
590        }
591    
592        public boolean isDoGenerate() {
593            return doGenerate;
594        }
595    
596        public void setForce(boolean force) {
597            this.force = force;
598        }
599    
600        public void setDoGenerate(boolean doGenerate) {
601            this.doGenerate = doGenerate;
602        }
603    
604        public boolean isDoGenerateBundle() {
605            return doGenerateBundle;
606        }
607    
608        public void setDoGenerateBundle(boolean doGenerateBundle) {
609            this.doGenerateBundle = doGenerateBundle;
610        }
611    
612        public List<String> getExcludedScopes() {
613            String[] split = excludedScopes == null ? new String[0] : excludedScopes.split(",");
614            return Arrays.asList(split);
615        }
616    
617        public void setExcludedScopes(String excludedScopes) {
618            this.excludedScopes = excludedScopes;
619        }
620    
621        public List<String> getIncludedScopes() {
622            String[] split = includedScopes == null ? new String[0] : includedScopes.split(",");
623            return Arrays.asList(split);
624        }
625    
626        public void setIncludedScopes(String includedScopes) {
627            this.includedScopes = includedScopes;
628        }
629    
630        public String getExcludedGroups() {
631            return excludedGroups;
632        }
633    
634        public void setExcludedGroups(String excludedGroups) {
635            this.excludedGroups = excludedGroups;
636        }
637    
638        public String getIncludedGroups() {
639            return includedGroups;
640        }
641    
642        public void setIncludedGroups(String includedGroups) {
643            this.includedGroups = includedGroups;
644        }
645    
646        public String getExcludedArtifacts() {
647            return excludedArtifacts;
648        }
649    
650        public void setExcludedArtifacts(String excludedArtifacts) {
651            this.excludedArtifacts = excludedArtifacts;
652        }
653    
654        public String getIncludedArtifacts() {
655            return includedArtifacts;
656        }
657    
658        public void setIncludedArtifacts(String includedArtifacts) {
659            this.includedArtifacts = includedArtifacts;
660        }
661    
662        public ThirdPartyTool getThridPartyTool() {
663            return thridPartyTool;
664        }
665    
666        public void setThridPartyTool(ThirdPartyTool thridPartyTool) {
667            this.thridPartyTool = thridPartyTool;
668        }
669    
670        public String getArtifactLicenseMapping() {
671            return artifactLicenseMapping;
672        }
673    
674        public void setArtifactLicenseMapping(String artifactLicenseMapping) {
675            this.artifactLicenseMapping = artifactLicenseMapping;
676        }
677    }