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 }