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                File propertiesFile = copyToFileSystem(getArtifactLicenseMapping());
315                setMissingFile(propertiesFile);
316            }
317    
318            Log log = getLog();
319    
320            if (log.isDebugEnabled()) {
321    
322                // always be verbose in debug mode
323                setVerbose(true);
324            }
325    
326            File file = new File(getOutputDirectory(), getThirdPartyFilename());
327    
328            setThirdPartyFile(file);
329    
330            long buildTimestamp = getBuildTimestamp();
331    
332            if (isVerbose()) {
333                log.info("Build start   at : " + buildTimestamp);
334                log.info("third-party file : " + file.lastModified());
335            }
336    
337            setDoGenerate(isForce() || !file.exists() || buildTimestamp > file.lastModified());
338    
339            if (isGenerateBundle()) {
340    
341                File bundleFile = FileUtil.getFile(getOutputDirectory(), getBundleThirdPartyPath());
342    
343                if (isVerbose()) {
344                    log.info("bundle third-party file : " + bundleFile.lastModified());
345                }
346                setDoGenerateBundle(isForce() || !bundleFile.exists() || buildTimestamp > bundleFile.lastModified());
347            } else {
348    
349                // not generating bundled file
350                setDoGenerateBundle(false);
351            }
352    
353            projectDependencies = loadDependencies();
354    
355            licenseMap = createLicenseMap(projectDependencies);
356    
357            SortedSet<MavenProject> unsafeDependencies = getThirdPartyTool().getProjectsWithNoLicense(licenseMap,
358                    isVerbose());
359    
360            setUnsafeDependencies(unsafeDependencies);
361    
362            if (!CollectionUtils.isEmpty(unsafeDependencies) && isUseMissingFile() && isDoGenerate()) {
363    
364                // load unsafeMapping
365                unsafeMappings = createUnsafeMapping();
366            }
367    
368            if (!CollectionUtils.isEmpty(licenseMerges)) {
369    
370                // check where is not multi licenses merged main licenses (see OJO-1723)
371                Map<String, String[]> mergedLicenses = new HashMap<String, String[]>();
372    
373                for (String merge : licenseMerges) {
374                    merge = merge.trim();
375                    String[] split = merge.split("\\|");
376    
377                    String mainLicense = split[0];
378    
379                    if (mergedLicenses.containsKey(mainLicense)) {
380    
381                        // this license was already describe, fail the build...
382    
383                        throw new MojoFailureException(
384                                "The merge main license "
385                                        + mainLicense
386                                        + " was already registred in the "
387                                        + "configuration, please use only one such entry as describe in example "
388                                        + "http://mojo.codehaus.org/license-maven-plugin/examples/example-thirdparty.html#Merge_licenses.");
389                    }
390                    mergedLicenses.put(mainLicense, split);
391                }
392    
393                // merge licenses in license map
394    
395                for (String[] mergedLicense : mergedLicenses.values()) {
396                    if (isVerbose()) {
397                        getLog().info("Will merge " + Arrays.toString(mergedLicense) + "");
398                    }
399    
400                    thirdPartyTool.mergeLicenses(licenseMap, mergedLicense);
401                }
402            }
403        }
404    
405        protected LicenseMap createLicenseMap(SortedMap<String, MavenProject> dependencies) {
406    
407            LicenseMap licenseMap = new LicenseMap();
408    
409            for (MavenProject project : dependencies.values()) {
410                thirdPartyTool.addLicense(licenseMap, project, project.getLicenses());
411            }
412            return licenseMap;
413        }
414    
415        protected boolean checkUnsafeDependencies() {
416            SortedSet<MavenProject> unsafeDependencies = getUnsafeDependencies();
417            boolean unsafe = !CollectionUtils.isEmpty(unsafeDependencies);
418            if (unsafe) {
419                Log log = getLog();
420                log.debug("There is " + unsafeDependencies.size() + " dependencies with no license :");
421                for (MavenProject dep : unsafeDependencies) {
422    
423                    // no license found for the dependency
424                    log.debug(" - " + MojoHelper.getArtifactId(dep.getArtifact()));
425                }
426            }
427            return unsafe;
428        }
429    
430        protected void writeThirdPartyFile() throws IOException {
431    
432            Log log = getLog();
433            LicenseMap licenseMap = getLicenseMap();
434            File target = getThirdPartyFile();
435    
436            if (isDoGenerate()) {
437                StringBuilder sb = new StringBuilder();
438                if (licenseMap.isEmpty()) {
439                    sb.append(NO_DEPENDENCIES_MESSAGE);
440                } else {
441                    if (isGroupByLicense()) {
442    
443                        // group by license
444                        sb.append("List of third-party dependencies grouped by " + "their license type.");
445                        for (String licenseName : licenseMap.keySet()) {
446                            SortedSet<MavenProject> projects = licenseMap.get(licenseName);
447    
448                            // Don't print the license if it isn't being used
449                            if (projects == null || projects.size() == 0) {
450                                continue;
451                            }
452    
453                            sb.append("\n\n").append(licenseName).append(" : ");
454    
455                            for (MavenProject mavenProject : projects) {
456                                String s = MojoHelper.getArtifactName(mavenProject);
457                                sb.append("\n  * ").append(s);
458                            }
459                        }
460    
461                    } else {
462    
463                        // group by dependencies
464                        SortedMap<MavenProject, String[]> map = licenseMap.toDependencyMap();
465    
466                        sb.append("List of ").append(map.size()).append(" third-party dependencies.\n");
467    
468                        List<String> lines = new ArrayList<String>();
469    
470                        for (Map.Entry<MavenProject, String[]> entry : map.entrySet()) {
471                            String artifact = MojoHelper.getArtifactName(entry.getKey());
472                            StringBuilder buffer = new StringBuilder();
473                            for (String license : entry.getValue()) {
474                                buffer.append(" (").append(license).append(")");
475                            }
476                            String licenses = buffer.toString();
477                            String line = licenses + " " + artifact;
478                            lines.add(line);
479                        }
480    
481                        Collections.sort(lines);
482                        for (String line : lines) {
483                            sb.append('\n').append(line);
484                        }
485                        lines.clear();
486                    }
487                }
488                String content = sb.toString();
489    
490                log.info("Writing third-party file to " + target);
491                if (isVerbose()) {
492                    log.info(content);
493                }
494    
495                FileUtil.writeString(target, content, getEncoding());
496            }
497    
498            if (isDoGenerateBundle()) {
499    
500                // creates the bundled license file
501                File bundleTarget = FileUtil.getFile(getOutputDirectory(), getBundleThirdPartyPath());
502                log.info("Writing bundled third-party file to " + bundleTarget);
503                FileUtil.copyFile(target, bundleTarget);
504            }
505        }
506    
507        public boolean isGroupByLicense() {
508            return groupByLicense;
509        }
510    
511        public void setGroupByLicense(boolean groupByLicense) {
512            this.groupByLicense = groupByLicense;
513        }
514    
515        public File getOutputDirectory() {
516            return outputDirectory;
517        }
518    
519        public String getThirdPartyFilename() {
520            return thirdPartyFilename;
521        }
522    
523        public String getBundleThirdPartyPath() {
524            return bundleThirdPartyPath;
525        }
526    
527        public boolean isGenerateBundle() {
528            return generateBundle;
529        }
530    
531        public boolean isFailIfWarning() {
532            return failIfWarning;
533        }
534    
535        public SortedMap<String, MavenProject> getProjectDependencies() {
536            return projectDependencies;
537        }
538    
539        public SortedSet<MavenProject> getUnsafeDependencies() {
540            return unsafeDependencies;
541        }
542    
543        public void setUnsafeDependencies(SortedSet<MavenProject> unsafeDependencies) {
544            this.unsafeDependencies = unsafeDependencies;
545        }
546    
547        public File getThirdPartyFile() {
548            return thirdPartyFile;
549        }
550    
551        public LicenseMap getLicenseMap() {
552            return licenseMap;
553        }
554    
555        public void setOutputDirectory(File outputDirectory) {
556            this.outputDirectory = outputDirectory;
557        }
558    
559        public void setThirdPartyFilename(String thirdPartyFilename) {
560            this.thirdPartyFilename = thirdPartyFilename;
561        }
562    
563        public void setBundleThirdPartyPath(String bundleThirdPartyPath) {
564            this.bundleThirdPartyPath = bundleThirdPartyPath;
565        }
566    
567        public void setGenerateBundle(boolean generateBundle) {
568            this.generateBundle = generateBundle;
569        }
570    
571        public void setThirdPartyFile(File thirdPartyFile) {
572            this.thirdPartyFile = thirdPartyFile;
573        }
574    
575        public boolean isUseMissingFile() {
576            return useMissingFile;
577        }
578    
579        public File getMissingFile() {
580            return missingFile;
581        }
582    
583        public void setUseMissingFile(boolean useMissingFile) {
584            this.useMissingFile = useMissingFile;
585        }
586    
587        public void setMissingFile(File missingFile) {
588            this.missingFile = missingFile;
589        }
590    
591        public void setFailIfWarning(boolean failIfWarning) {
592            this.failIfWarning = failIfWarning;
593        }
594    
595        public SortedProperties getUnsafeMappings() {
596            return unsafeMappings;
597        }
598    
599        public boolean isForce() {
600            return force;
601        }
602    
603        public boolean isDoGenerate() {
604            return doGenerate;
605        }
606    
607        public void setForce(boolean force) {
608            this.force = force;
609        }
610    
611        public void setDoGenerate(boolean doGenerate) {
612            this.doGenerate = doGenerate;
613        }
614    
615        public boolean isDoGenerateBundle() {
616            return doGenerateBundle;
617        }
618    
619        public void setDoGenerateBundle(boolean doGenerateBundle) {
620            this.doGenerateBundle = doGenerateBundle;
621        }
622    
623        public List<String> getExcludedScopes() {
624            String[] split = excludedScopes == null ? new String[0] : excludedScopes.split(",");
625            return Arrays.asList(split);
626        }
627    
628        public void setExcludedScopes(String excludedScopes) {
629            this.excludedScopes = excludedScopes;
630        }
631    
632        public List<String> getIncludedScopes() {
633            String[] split = includedScopes == null ? new String[0] : includedScopes.split(",");
634            return Arrays.asList(split);
635        }
636    
637        public void setIncludedScopes(String includedScopes) {
638            this.includedScopes = includedScopes;
639        }
640    
641        public String getExcludedGroups() {
642            return excludedGroups;
643        }
644    
645        public void setExcludedGroups(String excludedGroups) {
646            this.excludedGroups = excludedGroups;
647        }
648    
649        public String getIncludedGroups() {
650            return includedGroups;
651        }
652    
653        public void setIncludedGroups(String includedGroups) {
654            this.includedGroups = includedGroups;
655        }
656    
657        public String getExcludedArtifacts() {
658            return excludedArtifacts;
659        }
660    
661        public void setExcludedArtifacts(String excludedArtifacts) {
662            this.excludedArtifacts = excludedArtifacts;
663        }
664    
665        public String getIncludedArtifacts() {
666            return includedArtifacts;
667        }
668    
669        public void setIncludedArtifacts(String includedArtifacts) {
670            this.includedArtifacts = includedArtifacts;
671        }
672    
673        public ThirdPartyTool getThirdPartyTool() {
674            return thirdPartyTool;
675        }
676    
677        public void setThirdPartyTool(ThirdPartyTool thridPartyTool) {
678            this.thirdPartyTool = thridPartyTool;
679        }
680    
681        public String getArtifactLicenseMapping() {
682            return artifactLicenseMapping;
683        }
684    
685        public void setArtifactLicenseMapping(String artifactLicenseMapping) {
686            this.artifactLicenseMapping = artifactLicenseMapping;
687        }
688    }