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