001    /**
002     * Copyright 2010-2013 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. This
092         * properties file supports matching by groupId, groupId + artifactId, or groupId+artifactId+version.
093         *
094         * @parameter expression="${license.artifactLicenseMapping}" default-value="classpath:THIRD-PARTY.properties"
095         * @since 1.0
096         */
097        protected String artifactLicenseMapping;
098    
099        /**
100         * To merge licenses in final file.
101         * <p/>
102         * Each entry represents a merge (first license is main license to keep), licenses are separated by {@code |}.
103         * <p/>
104         * Example :
105         * <p/>
106         *
107         * <pre>
108         * &lt;licenseMerges&gt;
109         * &lt;licenseMerge&gt;The Apache Software License|Version 2.0,Apache License, Version 2.0&lt;/licenseMerge&gt;
110         * &lt;/licenseMerges&gt;
111         * &lt;/pre&gt;
112         *
113         * @parameter
114         * @since 1.0
115         */
116        protected List<String> licenseMerges;
117    
118        /**
119         * The path of the bundled third party file to produce when {@link #generateBundle} is on.
120         * <p/>
121         * <b>Note:</b> This option is not available for {@code pom} module types.
122         *
123         * @parameter expression="${license.bundleThirdPartyPath}"
124         *            default-value="META-INF/${project.artifactId}-THIRD-PARTY.txt"
125         * @since 1.0
126         */
127        protected String bundleThirdPartyPath;
128    
129        /**
130         * A flag to copy a bundled version of the third-party file. This is usefull to avoid for a final application
131         * collision name of third party file.
132         * <p/>
133         * The file will be copied at the {@link #bundleThirdPartyPath} location.
134         *
135         * @parameter expression="${license.generateBundle}" default-value="false"
136         * @since 1.0
137         */
138        protected boolean generateBundle;
139    
140        /**
141         * To force generation of the third-party file even if every thing is up to date.
142         *
143         * @parameter expression="${license.force}" default-value="false"
144         * @since 1.0
145         */
146        protected boolean force;
147    
148        /**
149         * A flag to fail the build if at least one dependency was detected without a license.
150         *
151         * @parameter expression="${license.failIfWarning}" default-value="false"
152         * @since 1.0
153         */
154        protected boolean failIfWarning;
155    
156        /**
157         * A flag to change the grouping of the generated THIRD-PARTY file.
158         * <p/>
159         * By default, group by dependencies.
160         * <p/>
161         * If sets to {@code true}, the it will group by license type.
162         *
163         * @parameter expression="${license.groupByLicense}" default-value="false"
164         * @since 1.0
165         */
166        protected boolean groupByLicense;
167    
168        /**
169         * A filter to exclude some scopes.
170         *
171         * @parameter expression="${license.excludedScopes}" default-value="system"
172         * @since 1.0
173         */
174        protected String excludedScopes;
175    
176        /**
177         * A filter to include only some scopes, if let empty then all scopes will be used (no filter).
178         *
179         * @parameter expression="${license.includedScopes}" default-value=""
180         * @since 1.0
181         */
182        protected String includedScopes;
183    
184        /**
185         * A filter to exclude some GroupIds
186         *
187         * @parameter expression="${license.excludedGroups}" default-value=""
188         * @since 1.0
189         */
190        protected String excludedGroups;
191    
192        /**
193         * A filter to include only some GroupIds
194         *
195         * @parameter expression="${license.includedGroups}" default-value=""
196         * @since 1.0
197         */
198        protected String includedGroups;
199    
200        /**
201         * A filter to exclude some ArtifactsIds
202         *
203         * @parameter expression="${license.excludedArtifacts}" default-value=""
204         * @since 1.0
205         */
206        protected String excludedArtifacts;
207    
208        /**
209         * A filter to include only some ArtifactsIds
210         *
211         * @parameter expression="${license.includedArtifacts}" default-value=""
212         * @since 1.0
213         */
214        protected String includedArtifacts;
215    
216        /**
217         * Include transitive dependencies when downloading license files.
218         *
219         * @parameter default-value="true"
220         * @since 1.0
221         */
222        protected boolean includeTransitiveDependencies;
223    
224        /**
225         * third party tool.
226         *
227         * @component
228         * @readonly
229         * @since 1.0
230         */
231        private ThirdPartyTool thirdPartyTool;
232    
233        private SortedMap<String, MavenProject> projectDependencies;
234    
235        private LicenseMap licenseMap;
236    
237        private SortedSet<MavenProject> unsafeDependencies;
238    
239        private File thirdPartyFile;
240    
241        private SortedProperties unsafeMappings;
242    
243        private boolean doGenerate;
244    
245        private boolean doGenerateBundle;
246    
247        public static final String NO_DEPENDENCIES_MESSAGE = "the project has no dependencies.";
248    
249        private static SortedMap<String, MavenProject> artifactCache;
250    
251        public static SortedMap<String, MavenProject> getArtifactCache() {
252            if (artifactCache == null) {
253                artifactCache = new TreeMap<String, MavenProject>();
254            }
255            return artifactCache;
256        }
257    
258        protected abstract SortedMap<String, MavenProject> loadDependencies();
259    
260        protected abstract SortedProperties createUnsafeMapping() throws ProjectBuildingException, IOException,
261                ThirdPartyToolException;
262    
263        protected boolean exists(String location) {
264            if (StringUtils.isBlank(location)) {
265                return false;
266            }
267            File file = new File(location);
268            if (file.exists()) {
269                return true;
270            }
271            ResourceLoader loader = new DefaultResourceLoader();
272            Resource resource = loader.getResource(location);
273            return resource.exists();
274        }
275    
276        protected InputStream getInputStream(String location) throws IOException {
277            File file = new File(location);
278            if (file.exists()) {
279                return new FileInputStream(file);
280            }
281            ResourceLoader loader = new DefaultResourceLoader();
282            Resource resource = loader.getResource(location);
283            if (!resource.exists()) {
284                throw new IllegalArgumentException("Can't open an input stream for " + location);
285            } else {
286                return resource.getInputStream();
287            }
288        }
289    
290        protected File copyToFileSystem(String location) {
291            File temp = new File(getProject().getBuild().getDirectory() + "/license/THIRD-PARTY.properties");
292            return copyToFileSystem(location, temp);
293        }
294    
295        protected File copyToFileSystem(String location, File file) {
296            InputStream in = null;
297            OutputStream out = null;
298            try {
299                in = getInputStream(location);
300                out = FileUtils.openOutputStream(file);
301                IOUtils.copy(in, out);
302                getLog().debug("Copied " + location + " to " + file);
303                return file;
304            } catch (IOException e) {
305                throw new IllegalArgumentException(e);
306            } finally {
307                IOUtils.closeQuietly(in);
308                IOUtils.closeQuietly(out);
309            }
310        }
311    
312        @Override
313        protected void init() throws Exception {
314            if (exists(getArtifactLicenseMapping())) {
315                // The artifact->license mapping file might be a URL, not a file
316                // This call always copies the mapping file from wherever it is to target/license/THIRD-PARTY.properties
317                // This way we are guaranteed to have a local copy of the mapping file to work with
318                File propertiesFile = copyToFileSystem(getArtifactLicenseMapping());
319                // "missingFile" contains a mapping between Maven GAV's and their corresponding license
320                setMissingFile(propertiesFile);
321            }
322    
323            Log log = getLog();
324            if (log.isDebugEnabled()) {
325                // always be verbose in debug mode
326                setVerbose(true);
327            }
328    
329            // This is the file that gets bundled into the jar as META-INF/THIRD-PARTY.txt
330            // It contains the aggregated list of licenses/jar's this project depends on
331            File file = new File(getOutputDirectory(), getThirdPartyFilename());
332    
333            setThirdPartyFile(file);
334    
335            long buildTimestamp = getBuildTimestamp();
336    
337            if (isVerbose()) {
338                log.info("Build start   at : " + buildTimestamp);
339                log.info("third-party file : " + file.lastModified());
340            }
341    
342            setDoGenerate(isForce() || !file.exists() || buildTimestamp > file.lastModified());
343    
344            if (isGenerateBundle()) {
345                File bundleFile = FileUtil.getFile(getOutputDirectory(), getBundleThirdPartyPath());
346                if (isVerbose()) {
347                    log.info("bundle third-party file : " + bundleFile.lastModified());
348                }
349                setDoGenerateBundle(isForce() || !bundleFile.exists() || buildTimestamp > bundleFile.lastModified());
350            } else {
351                // not generating bundled file
352                setDoGenerateBundle(false);
353            }
354    
355            // This is the complete, transitive list of dependencies of the current project
356            // It is stored as a map of MavenProjects keyed by GAV
357            // If the pom of the dep. includes the license(s) it is released under, project.getLicenses() returns that info
358            projectDependencies = loadDependencies();
359    
360            // This is also the complete, transitive list of dependencies of the current project
361            // However, it is stored as a map of Set<MavenProject>, where the key is the license name
362            licenseMap = createLicenseMap(projectDependencies);
363    
364            // These are the dependencies whose pom's don't include license info
365            SortedSet<MavenProject> unsafeDependencies = getThirdPartyTool().getProjectsWithNoLicense(licenseMap,
366                    isVerbose());
367    
368            setUnsafeDependencies(unsafeDependencies);
369    
370            if (!CollectionUtils.isEmpty(unsafeDependencies) && isUseMissingFile() && isDoGenerate()) {
371                // load unsafeMapping
372                unsafeMappings = createUnsafeMapping();
373            }
374    
375            if (!CollectionUtils.isEmpty(licenseMerges)) {
376    
377                // check where is not multi licenses merged main licenses (see OJO-1723)
378                Map<String, String[]> mergedLicenses = new HashMap<String, String[]>();
379    
380                for (String merge : licenseMerges) {
381                    merge = merge.trim();
382                    String[] split = merge.split("\\|");
383    
384                    String mainLicense = split[0];
385    
386                    if (mergedLicenses.containsKey(mainLicense)) {
387    
388                        // this license was already describe, fail the build...
389    
390                        throw new MojoFailureException(
391                                "The merge main license "
392                                        + mainLicense
393                                        + " was already registred in the "
394                                        + "configuration, please use only one such entry as describe in example "
395                                        + "http://mojo.codehaus.org/license-maven-plugin/examples/example-thirdparty.html#Merge_licenses.");
396                    }
397                    mergedLicenses.put(mainLicense, split);
398                }
399    
400                // merge licenses in license map
401    
402                for (String[] mergedLicense : mergedLicenses.values()) {
403                    if (isVerbose()) {
404                        getLog().info("Will merge " + Arrays.toString(mergedLicense) + "");
405                    }
406    
407                    thirdPartyTool.mergeLicenses(licenseMap, mergedLicense);
408                }
409            }
410        }
411    
412        /**
413         * This returns the complete transitive dependency tree keyed by license type after applying some cleanup
414         */
415        protected LicenseMap createLicenseMap(SortedMap<String, MavenProject> dependencies) {
416            LicenseMap licenseMap = new LicenseMap();
417            for (MavenProject dependency : dependencies.values()) {
418                thirdPartyTool.addLicense(licenseMap, dependency, dependency.getLicenses());
419            }
420            return licenseMap;
421        }
422    
423        protected boolean checkUnsafeDependencies() {
424            SortedSet<MavenProject> unsafeDependencies = getUnsafeDependencies();
425            boolean unsafe = !CollectionUtils.isEmpty(unsafeDependencies);
426            if (unsafe) {
427                Log log = getLog();
428                log.debug("There are " + unsafeDependencies.size() + " dependencies with no license :");
429                for (MavenProject dep : unsafeDependencies) {
430    
431                    // no license found for the dependency
432                    log.debug(" - " + MojoHelper.getArtifactId(dep.getArtifact()));
433                }
434            }
435            return unsafe;
436        }
437    
438        protected void writeThirdPartyFile() throws IOException {
439    
440            Log log = getLog();
441            LicenseMap licenseMap = getLicenseMap();
442            File target = getThirdPartyFile();
443    
444            if (isDoGenerate()) {
445                StringBuilder sb = new StringBuilder();
446                if (licenseMap.isEmpty()) {
447                    sb.append(NO_DEPENDENCIES_MESSAGE);
448                } else {
449                    if (isGroupByLicense()) {
450    
451                        // group by license
452                        sb.append("List of third-party dependencies grouped by " + "their license type.");
453                        for (String licenseName : licenseMap.keySet()) {
454                            SortedSet<MavenProject> projects = licenseMap.get(licenseName);
455    
456                            // Don't print the license if it isn't being used
457                            if (projects == null || projects.size() == 0) {
458                                continue;
459                            }
460    
461                            sb.append("\n\n").append(licenseName).append(" : ");
462    
463                            for (MavenProject mavenProject : projects) {
464                                String s = MojoHelper.getArtifactName(mavenProject);
465                                sb.append("\n  * ").append(s);
466                            }
467                        }
468    
469                    } else {
470    
471                        // group by dependencies
472                        SortedMap<MavenProject, String[]> map = licenseMap.toDependencyMap();
473    
474                        sb.append("List of ").append(map.size()).append(" third-party dependencies.\n");
475    
476                        List<String> lines = new ArrayList<String>();
477    
478                        for (Map.Entry<MavenProject, String[]> entry : map.entrySet()) {
479                            String artifact = MojoHelper.getArtifactName(entry.getKey());
480                            StringBuilder buffer = new StringBuilder();
481                            for (String license : entry.getValue()) {
482                                buffer.append(" (").append(license).append(")");
483                            }
484                            String licenses = buffer.toString();
485                            String line = licenses + " " + artifact;
486                            lines.add(line);
487                        }
488    
489                        Collections.sort(lines);
490                        for (String line : lines) {
491                            sb.append('\n').append(line);
492                        }
493                        lines.clear();
494                    }
495                }
496                String content = sb.toString();
497    
498                log.info("Writing third-party file to " + target);
499                if (isVerbose()) {
500                    log.info(content);
501                }
502    
503                FileUtil.writeString(target, content, getEncoding());
504            }
505    
506            if (isDoGenerateBundle()) {
507    
508                // creates the bundled license file
509                File bundleTarget = FileUtil.getFile(getOutputDirectory(), getBundleThirdPartyPath());
510                log.info("Writing bundled third-party file to " + bundleTarget);
511                FileUtil.copyFile(target, bundleTarget);
512            }
513        }
514    
515        public boolean isGroupByLicense() {
516            return groupByLicense;
517        }
518    
519        public void setGroupByLicense(boolean groupByLicense) {
520            this.groupByLicense = groupByLicense;
521        }
522    
523        public File getOutputDirectory() {
524            return outputDirectory;
525        }
526    
527        public String getThirdPartyFilename() {
528            return thirdPartyFilename;
529        }
530    
531        public String getBundleThirdPartyPath() {
532            return bundleThirdPartyPath;
533        }
534    
535        public boolean isGenerateBundle() {
536            return generateBundle;
537        }
538    
539        public boolean isFailIfWarning() {
540            return failIfWarning;
541        }
542    
543        public SortedMap<String, MavenProject> getProjectDependencies() {
544            return projectDependencies;
545        }
546    
547        public SortedSet<MavenProject> getUnsafeDependencies() {
548            return unsafeDependencies;
549        }
550    
551        public void setUnsafeDependencies(SortedSet<MavenProject> unsafeDependencies) {
552            this.unsafeDependencies = unsafeDependencies;
553        }
554    
555        public File getThirdPartyFile() {
556            return thirdPartyFile;
557        }
558    
559        public LicenseMap getLicenseMap() {
560            return licenseMap;
561        }
562    
563        public void setOutputDirectory(File outputDirectory) {
564            this.outputDirectory = outputDirectory;
565        }
566    
567        public void setThirdPartyFilename(String thirdPartyFilename) {
568            this.thirdPartyFilename = thirdPartyFilename;
569        }
570    
571        public void setBundleThirdPartyPath(String bundleThirdPartyPath) {
572            this.bundleThirdPartyPath = bundleThirdPartyPath;
573        }
574    
575        public void setGenerateBundle(boolean generateBundle) {
576            this.generateBundle = generateBundle;
577        }
578    
579        public void setThirdPartyFile(File thirdPartyFile) {
580            this.thirdPartyFile = thirdPartyFile;
581        }
582    
583        public boolean isUseMissingFile() {
584            return useMissingFile;
585        }
586    
587        public File getMissingFile() {
588            return missingFile;
589        }
590    
591        public void setUseMissingFile(boolean useMissingFile) {
592            this.useMissingFile = useMissingFile;
593        }
594    
595        public void setMissingFile(File missingFile) {
596            this.missingFile = missingFile;
597        }
598    
599        public void setFailIfWarning(boolean failIfWarning) {
600            this.failIfWarning = failIfWarning;
601        }
602    
603        public SortedProperties getUnsafeMappings() {
604            return unsafeMappings;
605        }
606    
607        public boolean isForce() {
608            return force;
609        }
610    
611        public boolean isDoGenerate() {
612            return doGenerate;
613        }
614    
615        public void setForce(boolean force) {
616            this.force = force;
617        }
618    
619        public void setDoGenerate(boolean doGenerate) {
620            this.doGenerate = doGenerate;
621        }
622    
623        public boolean isDoGenerateBundle() {
624            return doGenerateBundle;
625        }
626    
627        public void setDoGenerateBundle(boolean doGenerateBundle) {
628            this.doGenerateBundle = doGenerateBundle;
629        }
630    
631        public List<String> getExcludedScopes() {
632            String[] split = excludedScopes == null ? new String[0] : excludedScopes.split(",");
633            return Arrays.asList(split);
634        }
635    
636        public void setExcludedScopes(String excludedScopes) {
637            this.excludedScopes = excludedScopes;
638        }
639    
640        public List<String> getIncludedScopes() {
641            String[] split = includedScopes == null ? new String[0] : includedScopes.split(",");
642            return Arrays.asList(split);
643        }
644    
645        public void setIncludedScopes(String includedScopes) {
646            this.includedScopes = includedScopes;
647        }
648    
649        public String getExcludedGroups() {
650            return excludedGroups;
651        }
652    
653        public void setExcludedGroups(String excludedGroups) {
654            this.excludedGroups = excludedGroups;
655        }
656    
657        public String getIncludedGroups() {
658            return includedGroups;
659        }
660    
661        public void setIncludedGroups(String includedGroups) {
662            this.includedGroups = includedGroups;
663        }
664    
665        public String getExcludedArtifacts() {
666            return excludedArtifacts;
667        }
668    
669        public void setExcludedArtifacts(String excludedArtifacts) {
670            this.excludedArtifacts = excludedArtifacts;
671        }
672    
673        public String getIncludedArtifacts() {
674            return includedArtifacts;
675        }
676    
677        public void setIncludedArtifacts(String includedArtifacts) {
678            this.includedArtifacts = includedArtifacts;
679        }
680    
681        public ThirdPartyTool getThirdPartyTool() {
682            return thirdPartyTool;
683        }
684    
685        public void setThirdPartyTool(ThirdPartyTool thridPartyTool) {
686            this.thirdPartyTool = thridPartyTool;
687        }
688    
689        public String getArtifactLicenseMapping() {
690            return artifactLicenseMapping;
691        }
692    
693        public void setArtifactLicenseMapping(String artifactLicenseMapping) {
694            this.artifactLicenseMapping = artifactLicenseMapping;
695        }
696    }