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 }