001    /**
002     * Copyright 2010-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.codehaus.mojo.license;
017    
018    import java.io.File;
019    import java.io.FileInputStream;
020    import java.io.IOException;
021    import java.io.InputStream;
022    import java.io.OutputStream;
023    import java.util.ArrayList;
024    import java.util.Arrays;
025    import java.util.Collections;
026    import java.util.HashMap;
027    import java.util.List;
028    import java.util.Map;
029    import java.util.SortedMap;
030    import java.util.SortedSet;
031    import java.util.TreeMap;
032    
033    import org.apache.commons.collections.CollectionUtils;
034    import org.apache.commons.io.FileUtils;
035    import org.apache.commons.io.IOUtils;
036    import org.apache.commons.lang.StringUtils;
037    import org.apache.maven.plugin.MojoFailureException;
038    import org.apache.maven.plugin.logging.Log;
039    import org.apache.maven.project.MavenProject;
040    import org.apache.maven.project.ProjectBuildingException;
041    import org.codehaus.mojo.license.model.LicenseMap;
042    import org.springframework.core.io.DefaultResourceLoader;
043    import org.springframework.core.io.Resource;
044    import org.springframework.core.io.ResourceLoader;
045    
046    /**
047     * Abstract mojo for all third-party mojos.
048     *
049     * @author tchemit <chemit@codelutin.com>
050     * @since 1.0
051     */
052    public abstract class AbstractAddThirdPartyMojo extends AbstractLicenseMojo {
053    
054        /**
055         * Directory where to generate files.
056         *
057         * @parameter expression="${license.outputDirectory}"
058         *            default-value="${project.build.directory}/generated-sources/license"
059         * @required
060         * @since 1.0
061         */
062        protected File outputDirectory;
063    
064        /**
065         * File where license information for third party dependencies gets stored
066         *
067         * @parameter expression="${license.thirdPartyFilename}" default-value="THIRD-PARTY.txt"
068         * @required
069         * @since 1.0
070         */
071        protected String thirdPartyFilename;
072    
073        /**
074         * A flag to use the missing licenses file to consolidate the THID-PARTY file.
075         *
076         * @parameter expression="${license.useMissingFile}" default-value="false"
077         * @since 1.0
078         */
079        protected boolean useMissingFile;
080    
081        /**
082         * The file where to fill the license for dependencies with unknown license.
083         *
084         * @parameter expression="${license.missingFile}" default-value="src/license/THIRD-PARTY.properties"
085         * @since 1.0
086         */
087        protected File missingFile;
088    
089        /**
090         * Location of a properties file mapping artifacts without a license to the license that should be used for them.
091         * This supports classpath notation and any other type of URL Spring 3.1 resource loading can understand.
092         *
093         * @parameter expression="${license.artifactLicenseMapping}" default-value="classpath: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 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    }