001    /*
002     * #%L
003     * License Maven Plugin
004     *
005     * $Id: DefaultThirdPartyTool.java 14410 2011-08-10 20:54:51Z tchemit $
006     * $HeadURL: http://svn.codehaus.org/mojo/tags/license-maven-plugin-1.0/src/main/java/org/codehaus/mojo/license/DefaultThirdPartyTool.java $
007     * %%
008     * Copyright (C) 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    package org.codehaus.mojo.license;
026    
027    import java.io.File;
028    import java.io.IOException;
029    import java.util.ArrayList;
030    import java.util.Arrays;
031    import java.util.Collection;
032    import java.util.Comparator;
033    import java.util.HashMap;
034    import java.util.List;
035    import java.util.Map;
036    import java.util.Set;
037    import java.util.SortedMap;
038    import java.util.SortedSet;
039    import java.util.TreeSet;
040    import java.util.regex.Matcher;
041    import java.util.regex.Pattern;
042    
043    import org.apache.commons.collections.CollectionUtils;
044    import org.apache.commons.lang.StringUtils;
045    import org.apache.maven.artifact.Artifact;
046    import org.apache.maven.artifact.factory.ArtifactFactory;
047    import org.apache.maven.artifact.repository.ArtifactRepository;
048    import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
049    import org.apache.maven.artifact.resolver.ArtifactResolutionException;
050    import org.apache.maven.artifact.resolver.ArtifactResolver;
051    import org.apache.maven.model.License;
052    import org.apache.maven.project.MavenProject;
053    import org.apache.maven.project.MavenProjectBuilder;
054    import org.apache.maven.project.MavenProjectHelper;
055    import org.codehaus.mojo.license.model.LicenseMap;
056    import org.codehaus.plexus.logging.AbstractLogEnabled;
057    import org.codehaus.plexus.logging.Logger;
058    
059    /**
060     * Default implementation of the third party tool.
061     *
062     * @author <a href="mailto:tchemit@codelutin.com">Tony Chemit</a>
063     * @version $Id: DefaultThirdPartyTool.java 14410 2011-08-10 20:54:51Z tchemit $
064     * @plexus.component role="org.codehaus.mojo.license.ThirdPartyTool" role-hint="default"
065     */
066    public class DefaultThirdPartyTool extends AbstractLogEnabled implements ThirdPartyTool {
067        public static final String DESCRIPTOR_CLASSIFIER = "third-party";
068    
069        public static final String DESCRIPTOR_TYPE = "properties";
070    
071        // ----------------------------------------------------------------------
072        // Components
073        // ----------------------------------------------------------------------
074    
075        /**
076         * The component that is used to resolve additional artifacts required.
077         *
078         * @plexus.requirement
079         */
080        private ArtifactResolver artifactResolver;
081    
082        /**
083         * The component used for creating artifact instances.
084         *
085         * @plexus.requirement
086         */
087        private ArtifactFactory artifactFactory;
088    
089        /**
090         * Project builder.
091         *
092         * @plexus.requirement
093         */
094        private MavenProjectBuilder mavenProjectBuilder;
095    
096        /**
097         * Maven ProjectHelper.
098         *
099         * @plexus.requirement
100         */
101        private MavenProjectHelper projectHelper;
102    
103        /**
104         * Maven project comparator.
105         */
106        private final Comparator<MavenProject> projectComparator = MojoHelper.newMavenProjectComparator();
107    
108        /**
109         * {@inheritDoc}
110         */
111        @Override
112        public void attachThirdPartyDescriptor(MavenProject project, File file) {
113    
114            projectHelper.attachArtifact(project, DESCRIPTOR_TYPE, DESCRIPTOR_CLASSIFIER, file);
115        }
116    
117        /**
118         * {@inheritDoc}
119         */
120        @Override
121        public SortedSet<MavenProject> getProjectsWithNoLicense(LicenseMap licenseMap, boolean doLog) {
122    
123            Logger log = getLogger();
124    
125            // get unsafe dependencies (says with no license)
126            SortedSet<MavenProject> unsafeDependencies = licenseMap.get(LicenseMap.getUnknownLicenseMessage());
127    
128            if (doLog) {
129                if (CollectionUtils.isEmpty(unsafeDependencies)) {
130                    log.debug("There is no dependency with no license from poms.");
131                } else {
132                    log.debug("There is " + unsafeDependencies.size() + " dependencies with no license from poms : ");
133                    for (MavenProject dep : unsafeDependencies) {
134    
135                        // no license found for the dependency
136                        log.debug(" - " + MojoHelper.getArtifactId(dep.getArtifact()));
137                    }
138                }
139            }
140    
141            return unsafeDependencies;
142        }
143    
144        /**
145         * {@inheritDoc}
146         */
147        @Override
148        public SortedProperties loadThirdPartyDescriptorsForUnsafeMapping(String encoding,
149                Collection<MavenProject> projects, SortedSet<MavenProject> unsafeDependencies, LicenseMap licenseMap,
150                ArtifactRepository localRepository, List<ArtifactRepository> remoteRepositories)
151                throws ThirdPartyToolException, IOException {
152    
153            SortedProperties result = new SortedProperties(encoding);
154            Map<String, MavenProject> unsafeProjects = new HashMap<String, MavenProject>();
155            for (MavenProject unsafeDependency : unsafeDependencies) {
156                String id = MojoHelper.getArtifactId(unsafeDependency.getArtifact());
157                unsafeProjects.put(id, unsafeDependency);
158            }
159    
160            for (MavenProject mavenProject : projects) {
161    
162                if (CollectionUtils.isEmpty(unsafeDependencies)) {
163    
164                    // no more unsafe dependencies to find
165                    break;
166                }
167    
168                File thirdPartyDescriptor = resolvThirdPartyDescriptor(mavenProject, localRepository, remoteRepositories);
169    
170                if (thirdPartyDescriptor != null && thirdPartyDescriptor.exists() && thirdPartyDescriptor.length() > 0) {
171    
172                    if (getLogger().isInfoEnabled()) {
173                        getLogger().info("Detects third party descriptor " + thirdPartyDescriptor);
174                    }
175    
176                    // there is a third party file detected form the given dependency
177                    SortedProperties unsafeMappings = new SortedProperties(encoding);
178    
179                    if (thirdPartyDescriptor.exists()) {
180    
181                        getLogger().debug("Load missing file " + thirdPartyDescriptor);
182    
183                        // load the missing file
184                        unsafeMappings.load(thirdPartyDescriptor);
185                    }
186    
187                    for (String id : unsafeProjects.keySet()) {
188    
189                        if (unsafeMappings.containsKey(id)) {
190    
191                            String license = (String) unsafeMappings.get(id);
192                            if (StringUtils.isEmpty(license)) {
193    
194                                // empty license means not fill, skip it
195                                continue;
196                            }
197    
198                            // found a resolved unsafe dependency in the missing third party file
199                            MavenProject resolvedProject = unsafeProjects.get(id);
200                            unsafeDependencies.remove(resolvedProject);
201    
202                            // push back to
203                            result.put(id, license.trim());
204    
205                            addLicense(licenseMap, resolvedProject, license);
206                        }
207                    }
208                }
209            }
210            return result;
211        }
212    
213        /**
214         * {@inheritDoc}
215         */
216        @Override
217        public File resolvThirdPartyDescriptor(MavenProject project, ArtifactRepository localRepository,
218                List<ArtifactRepository> repositories) throws ThirdPartyToolException {
219            if (project == null) {
220                throw new IllegalArgumentException("The parameter 'project' can not be null");
221            }
222            if (localRepository == null) {
223                throw new IllegalArgumentException("The parameter 'localRepository' can not be null");
224            }
225            if (repositories == null) {
226                throw new IllegalArgumentException("The parameter 'remoteArtifactRepositories' can not be null");
227            }
228    
229            try {
230                return resolveThirdPartyDescriptor(project, localRepository, repositories);
231            } catch (ArtifactNotFoundException e) {
232                getLogger().debug("ArtifactNotFoundException: Unable to locate third party descriptor: " + e);
233                return null;
234            } catch (ArtifactResolutionException e) {
235                throw new ThirdPartyToolException("ArtifactResolutionException: Unable to locate third party descriptor: "
236                        + e.getMessage(), e);
237            } catch (IOException e) {
238                throw new ThirdPartyToolException(
239                        "IOException: Unable to locate third party descriptor: " + e.getMessage(), e);
240            }
241        }
242    
243        /**
244         * {@inheritDoc}
245         */
246        @Override
247        public void addLicense(LicenseMap licenseMap, MavenProject project, String licenseName) {
248            License license = new License();
249            license.setName(licenseName.trim());
250            license.setUrl(licenseName.trim());
251            addLicense(licenseMap, project, license);
252        }
253    
254        /**
255         * {@inheritDoc}
256         */
257        @Override
258        public void addLicense(LicenseMap licenseMap, MavenProject project, License license) {
259            addLicense(licenseMap, project, Arrays.asList(license));
260        }
261    
262        /**
263         * {@inheritDoc}
264         */
265        @Override
266        public void addLicense(LicenseMap licenseMap, MavenProject project, List<?> licenses) {
267    
268            if (Artifact.SCOPE_SYSTEM.equals(project.getArtifact().getScope())) {
269    
270                // do NOT treate system dependency
271                return;
272            }
273    
274            if (CollectionUtils.isEmpty(licenses)) {
275    
276                // no license found for the dependency
277                licenseMap.put(LicenseMap.getUnknownLicenseMessage(), project);
278                return;
279            }
280    
281            for (Object o : licenses) {
282                String id = MojoHelper.getArtifactId(project.getArtifact());
283                if (o == null) {
284                    getLogger().warn("could not acquire the license for " + id);
285                    continue;
286                }
287                License license = (License) o;
288                String licenseKey = license.getName();
289    
290                // tchemit 2010-08-29 Ano #816 Check if the License object is well formed
291    
292                if (StringUtils.isEmpty(license.getName())) {
293                    getLogger().debug("No license name defined for " + id);
294                    licenseKey = license.getUrl();
295                }
296    
297                if (StringUtils.isEmpty(licenseKey)) {
298                    getLogger().debug("No license url defined for " + id);
299                    licenseKey = LicenseMap.getUnknownLicenseMessage();
300                }
301                licenseMap.put(licenseKey, project);
302            }
303        }
304    
305        /**
306         * {@inheritDoc}
307         */
308        @Override
309        public void mergeLicenses(LicenseMap licenseMap, String... licenses) {
310            if (licenses.length == 0) {
311                return;
312            }
313    
314            String mainLicense = licenses[0].trim();
315            SortedSet<MavenProject> mainSet = licenseMap.get(mainLicense);
316            if (mainSet == null) {
317                getLogger().debug("No license [" + mainLicense + "] found, will create it.");
318                mainSet = new TreeSet<MavenProject>(projectComparator);
319                licenseMap.put(mainLicense, mainSet);
320            }
321            int size = licenses.length;
322            for (int i = 1; i < size; i++) {
323                String license = licenses[i].trim();
324                SortedSet<MavenProject> set = licenseMap.get(license);
325                if (set == null) {
326                    getLogger().debug("No license [" + license + "] found, skip this merge.");
327                    continue;
328                }
329                getLogger().debug("Merge license [" + license + "] (" + set.size() + " depedencies).");
330                mainSet.addAll(set);
331                set.clear();
332                licenseMap.remove(license);
333            }
334        }
335    
336        /**
337         * {@inheritDoc}
338         */
339        @Override
340        public SortedProperties loadUnsafeMapping(LicenseMap licenseMap, SortedMap<String, MavenProject> artifactCache,
341                String encoding, File missingFile) throws IOException {
342            SortedSet<MavenProject> unsafeDependencies = getProjectsWithNoLicense(licenseMap, false);
343    
344            SortedProperties unsafeMappings = new SortedProperties(encoding);
345    
346            if (missingFile.exists()) {
347                // there is some unsafe dependencies
348    
349                getLogger().debug("Load missing file " + missingFile);
350    
351                // load the missing file
352                unsafeMappings.load(missingFile);
353            }
354    
355            // get from the missing file, all unknown dependencies
356            List<String> unknownDependenciesId = new ArrayList<String>();
357    
358            // coming from maven-licen-plugin, we used in id type and classifier, now we remove
359            // these informations since GAV is good enough to qualify a license of any artifact of it...
360            Map<String, String> migrateKeys = migrateMissingFileKeys(unsafeMappings.keySet());
361    
362            for (Object o : migrateKeys.keySet()) {
363                String id = (String) o;
364                String migratedId = migrateKeys.get(id);
365    
366                MavenProject project = artifactCache.get(migratedId);
367                if (project == null) {
368                    // now we are sure this is a unknown dependency
369                    unknownDependenciesId.add(id);
370                } else {
371                    if (!id.equals(migratedId)) {
372    
373                        // migrates id to migratedId
374                        getLogger().info("Migrates [" + id + "] to [" + migratedId + "] in the missing file.");
375                        Object value = unsafeMappings.get(id);
376                        unsafeMappings.remove(id);
377                        unsafeMappings.put(migratedId, value);
378                    }
379                }
380            }
381    
382            if (!unknownDependenciesId.isEmpty()) {
383    
384                // there is some unknown dependencies in the missing file, remove them
385                for (String id : unknownDependenciesId) {
386                    getLogger().warn("dependency [" + id + "] does not exist in project, remove it from the missing file.");
387                    unsafeMappings.remove(id);
388                }
389    
390                unknownDependenciesId.clear();
391            }
392    
393            // push back loaded dependencies
394            for (Object o : unsafeMappings.keySet()) {
395                String id = (String) o;
396    
397                MavenProject project = artifactCache.get(id);
398                if (project == null) {
399                    getLogger().warn("dependency [" + id + "] does not exist in project.");
400                    continue;
401                }
402    
403                String license = (String) unsafeMappings.get(id);
404                if (StringUtils.isEmpty(license)) {
405    
406                    // empty license means not fill, skip it
407                    continue;
408                }
409    
410                // add license in map
411                addLicense(licenseMap, project, license);
412    
413                // remove unknown license
414                unsafeDependencies.remove(project);
415            }
416    
417            if (unsafeDependencies.isEmpty()) {
418    
419                // no more unknown license in map
420                licenseMap.remove(LicenseMap.getUnknownLicenseMessage());
421            } else {
422    
423                // add a "with no value license" for missing dependencies
424                for (MavenProject project : unsafeDependencies) {
425                    String id = MojoHelper.getArtifactId(project.getArtifact());
426                    if (getLogger().isDebugEnabled()) {
427                        getLogger().debug("dependency [" + id + "] has no license, add it in the missing file.");
428                    }
429                    unsafeMappings.setProperty(id, "");
430                }
431            }
432            return unsafeMappings;
433        }
434    
435        // ----------------------------------------------------------------------
436        // Private methods
437        // ----------------------------------------------------------------------
438    
439        /**
440         * @param project
441         *            not null
442         * @param localRepository
443         *            not null
444         * @param repositories
445         *            not null
446         * @return the resolved site descriptor
447         * @throws IOException
448         *             if any
449         * @throws ArtifactResolutionException
450         *             if any
451         * @throws ArtifactNotFoundException
452         *             if any
453         */
454        private File resolveThirdPartyDescriptor(MavenProject project, ArtifactRepository localRepository,
455                List<ArtifactRepository> repositories) throws IOException, ArtifactResolutionException,
456                ArtifactNotFoundException {
457            File result;
458    
459            // TODO: this is a bit crude - proper type, or proper handling as metadata rather than an artifact in 2.1?
460            Artifact artifact = artifactFactory.createArtifactWithClassifier(project.getGroupId(), project.getArtifactId(),
461                    project.getVersion(), DESCRIPTOR_TYPE, DESCRIPTOR_CLASSIFIER);
462            try {
463                artifactResolver.resolve(artifact, repositories, localRepository);
464    
465                result = artifact.getFile();
466    
467                // we use zero length files to avoid re-resolution (see below)
468                if (result.length() == 0) {
469                    getLogger().debug("Skipped third party descriptor");
470                }
471            } catch (ArtifactNotFoundException e) {
472                getLogger().debug("Unable to locate third party files descriptor : " + e);
473    
474                // we can afford to write an empty descriptor here as we don't expect it to turn up later in the remote
475                // repository, because the parent was already released (and snapshots are updated automatically if changed)
476                result = new File(localRepository.getBasedir(), localRepository.pathOf(artifact));
477    
478                FileUtil.createNewFile(result);
479            }
480    
481            return result;
482        }
483    
484        private final Pattern GAV_PLUS_TYPE_PATTERN = Pattern.compile("(.+)--(.+)--(.+)--(.+)");
485    
486        private final Pattern GAV_PLUS_TYPE_AND_CLASSIFIER_PATTERN = Pattern.compile("(.+)--(.+)--(.+)--(.+)--(.+)");
487    
488        private Map<String, String> migrateMissingFileKeys(Set<Object> missingFileKeys) {
489            Map<String, String> migrateKeys = new HashMap<String, String>();
490            for (Object object : missingFileKeys) {
491                String id = (String) object;
492                Matcher matcher;
493    
494                String newId = id;
495                matcher = GAV_PLUS_TYPE_AND_CLASSIFIER_PATTERN.matcher(id);
496                if (matcher.matches()) {
497                    newId = matcher.group(1) + "--" + matcher.group(2) + "--" + matcher.group(3);
498    
499                } else {
500                    matcher = GAV_PLUS_TYPE_PATTERN.matcher(id);
501                    if (matcher.matches()) {
502                        newId = matcher.group(1) + "--" + matcher.group(2) + "--" + matcher.group(3);
503    
504                    }
505                }
506                migrateKeys.put(id, newId);
507            }
508            return migrateKeys;
509        }
510    }