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