View Javadoc

1   /**
2    * Copyright 2010-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.codehaus.mojo.license;
17  
18  import java.io.File;
19  import java.io.IOException;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.Comparator;
24  import java.util.HashMap;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Properties;
29  import java.util.Set;
30  import java.util.SortedMap;
31  import java.util.SortedSet;
32  import java.util.TreeSet;
33  import java.util.regex.Matcher;
34  import java.util.regex.Pattern;
35  
36  import org.apache.commons.collections.CollectionUtils;
37  import org.apache.commons.lang.StringUtils;
38  import org.apache.maven.artifact.Artifact;
39  import org.apache.maven.artifact.factory.ArtifactFactory;
40  import org.apache.maven.artifact.repository.ArtifactRepository;
41  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
42  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
43  import org.apache.maven.artifact.resolver.ArtifactResolver;
44  import org.apache.maven.model.License;
45  import org.apache.maven.project.MavenProject;
46  import org.apache.maven.project.MavenProjectBuilder;
47  import org.apache.maven.project.MavenProjectHelper;
48  import org.codehaus.mojo.license.model.LicenseMap;
49  import org.codehaus.plexus.logging.AbstractLogEnabled;
50  import org.codehaus.plexus.logging.Logger;
51  
52  /**
53   * Default implementation of the third party tool.
54   *
55   * @author <a href="mailto:tchemit@codelutin.com">Tony Chemit</a>
56   * @version $Id: DefaultThirdPartyTool.java 14410 2011-08-10 20:54:51Z tchemit $
57   * @plexus.component role="org.codehaus.mojo.license.ThirdPartyTool" role-hint="default"
58   */
59  public class DefaultThirdPartyTool extends AbstractLogEnabled implements ThirdPartyTool {
60  
61      public static final String DESCRIPTOR_CLASSIFIER = "third-party";
62  
63      public static final String DESCRIPTOR_TYPE = "properties";
64  
65      // ----------------------------------------------------------------------
66      // Components
67      // ----------------------------------------------------------------------
68  
69      /**
70       * The component that is used to resolve additional artifacts required.
71       *
72       * @plexus.requirement
73       */
74      private ArtifactResolver artifactResolver;
75  
76      /**
77       * The component used for creating artifact instances.
78       *
79       * @plexus.requirement
80       */
81      private ArtifactFactory artifactFactory;
82  
83      /**
84       * Project builder.
85       *
86       * @plexus.requirement
87       */
88      private MavenProjectBuilder mavenProjectBuilder;
89  
90      /**
91       * Maven ProjectHelper.
92       *
93       * @plexus.requirement
94       */
95      private MavenProjectHelper projectHelper;
96  
97      /**
98       * Maven project comparator.
99       */
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().info(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 }