/******************************************************************************** * CruiseControl, a Continuous Integration Toolkit * Copyright (c) 2001, ThoughtWorks, Inc. * 200 E. Randolph, 25th Floor * Chicago, IL 60601 USA * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * + Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * + Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ********************************************************************************/ package net.sourceforge.cruisecontrol.sourcecontrols; import java.io.File; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.Modification; import net.sourceforge.cruisecontrol.SourceControl; import net.sourceforge.cruisecontrol.util.ValidationHelper; import org.apache.log4j.Logger; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.resolver.ArtifactNotFoundException; import org.apache.maven.artifact.resolver.ArtifactResolutionException; import org.apache.maven.cli.ConsoleDownloadMonitor; import org.apache.maven.embedder.MavenEmbedder; import org.apache.maven.embedder.MavenEmbedderConsoleLogger; import org.apache.maven.embedder.MavenEmbedderException; import org.apache.maven.model.Dependency; import org.apache.maven.model.Model; import org.apache.maven.project.MavenProject; import org.apache.maven.project.ProjectBuildingException; import org.apache.maven.wagon.events.TransferEvent; import org.apache.maven.wagon.events.TransferListener; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; /** * Checks snapshot dependencies listed in a Maven2 pom against the local repositorty. * * Date: Feb 8, 2006 * Time: 9:15:47 PM * * @author Dan Rollo */ public class Maven2SnapshotDependency implements SourceControl { /** enable logging for this class */ private static final Logger LOG = Logger.getLogger(Maven2SnapshotDependency.class); private final SourceControlProperties properties = new SourceControlProperties(); private List<Modification> modifications; private File pomFile; private String user; private File localRepoDir; //@todo Must be null until maven embedder honors alignWithUserInstallation. static final String COMMENT_TIMESTAMP_CHANGE = " timestamp change detected: "; static final String COMMENT_MISSING_IN_LOCALREPO = " missing in local repo: "; /** * @param s the pom.xml file who's snapshot dependencies we are going to scan */ public void setPomFile(final String s) { pomFile = new File(s); } /** * @param s the path for the local Maven repository. * Normally, this is not set in order to use the default location: user.home/.m2/repository. */ //@todo Make "public" when maven embedder honors alignWithUserInstallation void setLocalRepository(final String s) { if (s != null) { localRepoDir = new File(s); } else { localRepoDir = null; } } /** * @param s the username listed with changes found in binary dependencies */ public void setUser(final String s) { user = s; } public void setProperty(final String property) { properties.assignPropertyName(property); } public Map<String, String> getProperties() { return properties.getPropertiesAndReset(); } public void validate() throws CruiseControlException { ValidationHelper.assertIsSet(pomFile, "pomFile", this.getClass()); ValidationHelper.assertTrue(pomFile.exists(), "Pom file '" + pomFile.getAbsolutePath() + "' does not exist."); ValidationHelper.assertFalse(pomFile.isDirectory(), "The directory '" + pomFile.getAbsolutePath() + "' cannot be used as the pomFile for Maven2SnapshotDependency."); if (localRepoDir != null) { ValidationHelper.assertTrue(localRepoDir.exists(), "Local Maven repository '" + localRepoDir.getAbsolutePath() + "' does not exist."); ValidationHelper.assertTrue(localRepoDir.isDirectory(), "Local Maven repository '" + localRepoDir.getAbsolutePath() + "' must be a directory."); } } /** * The quiet period is ignored. All dependencies changed since the last * build trigger a modification. * * @param lastBuild * date of last build * @param now * IGNORED */ public List<Modification> getModifications(final Date lastBuild, final Date now) { modifications = new ArrayList<Modification>(); LOG.debug("Reading pom: " + pomFile.getAbsolutePath() + " with lastBuild: " + lastBuild); final ArtifactInfo[] artifactsToCheck = getSnapshotInfos(); for (final ArtifactInfo artifactToCheck : artifactsToCheck) { checkFile(artifactToCheck, lastBuild.getTime()); } return modifications; } /** * Add a Modification to the list of modifications. All modifications are * listed as "change" or "missing" if not in local repo. * @param dependency snapshot detected as modified * @param changeType modification type ("change" or "missing") * @param comment constant note according to changeType */ private void addRevision(final File dependency, final String changeType, final String comment) { final Modification newMod = new Modification("maven2"); final Modification.ModifiedFile modfile = newMod.createModifiedFile(dependency.getName(), dependency.getParent()); modfile.action = changeType; newMod.userName = user; newMod.modifiedTime = new Date(dependency.lastModified()); newMod.comment = comment; modifications.add(newMod); properties.modificationFound(); } /** Immutable data holder class. */ static final class ArtifactInfo { static final String ART_TYPE_PARENT = "parent"; static final String ART_TYPE_DEPENDENCY = "dependency"; private final Artifact artifact; private final String artifactType; private final File localRepoFile; private ArtifactInfo(final Artifact artifact, final String artifactType, File localRepoFile) { this.artifact = artifact; this.artifactType = artifactType; this.localRepoFile = localRepoFile; } Artifact getArtifact() { return artifact; } String getArtifactType() { return artifactType; } File getLocalRepoFile() { return localRepoFile; } public String toString() { return artifact + "," + artifactType + "," + (localRepoFile != null ? localRepoFile.getAbsolutePath() : null); } } /** * Return a file referring to the given artifact in the local repository. * @param localRepoBaseDir the actual base dir of the active local repository * @param artifact a artifact to be checked in the local repository * @return a file referring to the given artifact in the local repository */ //@todo Maybe we can delete this whole method after a while. private static File getArtifactFilename(final File localRepoBaseDir, final Artifact artifact) { LOG.warn("We should not need this approach to finding artifact files. Artifact: " + artifact); // Format: // ${repo}/${groupId,dots as dirs}/${artifactId}/${version}/${artifactId}-${version}[-${classifier}].${type} final StringBuilder fileName = new StringBuilder(); fileName.append(localRepoBaseDir.getAbsolutePath()); fileName.append('/'); fileName.append(artifact.getGroupId().replace('.', '/')); fileName.append('/'); final String artifactId = artifact.getArtifactId(); fileName.append(artifactId); fileName.append('/'); final String versionText = artifact.getVersion(); fileName.append(versionText); fileName.append('/'); fileName.append(artifactId); fileName.append('-'); fileName.append(versionText); if (artifact.getClassifier() != null) { fileName.append('-'); fileName.append(artifact.getClassifier()); } fileName.append('.'); final String type = artifact.getType(); fileName.append(type != null ? type : "jar"); //@todo Handle type="system" and "systemPath", or not if we can delete this whole method. return new File(fileName.toString()); } /** * Parse the Maven pom file, and return snapshot artifact info populated with dependencies to be checked. * @return return snapshot artifact info populated with dependencies to be checked */ ArtifactInfo[] getSnapshotInfos() { final MavenEmbedder embedder = getMvnEmbedder(); try { // With readProjectWithDependencies(), local repo dependencies (+transitive) will be updated if possible final MavenProject projectWithDependencies = getProjectWithDependencies(embedder, pomFile); // use local repo dir from embedder because this is the dir it is actually using final File localRepoBaseDir = new File(embedder.getLocalRepository().getBasedir()); final List<ArtifactInfo> artifactInfos = new ArrayList<ArtifactInfo>(); // handle parents and grandparents... findParentSnapshotArtifacts(projectWithDependencies, artifactInfos, localRepoBaseDir, embedder, pomFile); // handle dependencies final Set<Artifact> snapshotArtifacts; if (projectWithDependencies != null) { // projectWithDependencies.getDependencyArtifacts() would exclude transitive artifacts snapshotArtifacts = getSnaphotArtifacts(projectWithDependencies.getArtifacts()); } else { // couldn't read project, so try to do some stuff manually snapshotArtifacts = getSnapshotArtifactsManually(embedder); } for (final Artifact artifact : snapshotArtifacts) { addArtifactInfo(artifactInfos, artifact, ArtifactInfo.ART_TYPE_DEPENDENCY, localRepoBaseDir); } return artifactInfos.toArray(new ArtifactInfo[artifactInfos.size()]); } finally { try { embedder.stop(); } catch (MavenEmbedderException e) { LOG.error("Failed to stop embedded maven2", e); } } } private static void findParentSnapshotArtifacts(final MavenProject projectWithDependencies, final List<ArtifactInfo> artifactInfos, final File localRepoBaseDir, final MavenEmbedder embedder, final File pomFile) { // handle parents and grandparents... if (projectWithDependencies != null) { MavenProject currMvnProject = projectWithDependencies; Artifact parentArtifact = currMvnProject.getParentArtifact(); while ((parentArtifact != null) && parentArtifact.isSnapshot()) { addArtifactInfo(artifactInfos, parentArtifact, ArtifactInfo.ART_TYPE_PARENT, localRepoBaseDir); currMvnProject = currMvnProject.getParent(); parentArtifact = currMvnProject.getParentArtifact(); } } else { // couldn't read project, so try to do some stuff manually MavenProject mavenProject = null; try { mavenProject = embedder.readProject(pomFile); } catch (ProjectBuildingException e) { LOG.error("Failed to read maven2 mavenProject", e); } if (mavenProject != null) { MavenProject currMvnProject = mavenProject; Artifact artifact = currMvnProject.getParentArtifact(); while ((artifact != null) && (artifact.getVersion().endsWith(Artifact.SNAPSHOT_VERSION) || artifact.isSnapshot()) ) { addArtifactInfo(artifactInfos, artifact, ArtifactInfo.ART_TYPE_PARENT, localRepoBaseDir); resolveArtifact(embedder, artifact, mavenProject, embedder.getLocalRepository()); currMvnProject = currMvnProject.getParent(); artifact = currMvnProject.getParentArtifact(); } } } } private static MavenProject getProjectWithDependencies(final MavenEmbedder embedder, final File pomFile) { // With readProjectWithDependencies(), local repo dependencies (+transitive) will be updated if possible MavenProject projectWithDependencies = null; try { final TransferListener transferListener = new ConsoleDownloadMonitor() { public void transferProgress(TransferEvent transferEvent, byte[] buffer, int length) { // do nothing to avoid lot's of progress messages in logs } }; projectWithDependencies = embedder.readProjectWithDependencies(pomFile, transferListener); } catch (ProjectBuildingException e) { LOG.error("Failed to read maven2 projectWithDependencies", e); } catch (ArtifactResolutionException e) { LOG.warn("Failed to resolve artifact", e); } catch (ArtifactNotFoundException e) { LOG.warn("Couldn't find artifact", e); } return projectWithDependencies; } private static void resolveArtifact(final MavenEmbedder embedder, final Artifact artifact, final MavenProject mavenProject, final ArtifactRepository localRepo) { try { embedder.resolve(artifact, mavenProject.getPluginArtifactRepositories(), localRepo); } catch (ArtifactResolutionException e) { LOG.debug("Unresolved artifact", e); } catch (ArtifactNotFoundException e) { LOG.debug("Missing artifact", e); } } private static void addArtifactInfo(final List<ArtifactInfo> artifactInfos, final Artifact artifact, final String artifactType, final File localRepoBaseDir) { final File file; if (artifact.getFile() == null) { file = getArtifactFilename(localRepoBaseDir, artifact); } else { file = artifact.getFile(); } artifactInfos.add(new ArtifactInfo(artifact, artifactType, file)); } /** * Filter out non-SNAPSHOT artifacts. * @param artifacts all project artifacts, including non-SNAPSHOTS * @return a set of artifacts containing only SNAPSHOTs */ private static Set<Artifact> getSnaphotArtifacts(final Set artifacts) { final Set<Artifact> retVal = new HashSet<Artifact>(); for (Object artifact1 : artifacts) { final Artifact artifact = (Artifact) artifact1; LOG.debug("Examining artifact: " + artifact); if (artifact.isSnapshot()) { retVal.add(artifact); } } return retVal; } /** * Doesn't handle transitive deps, nor actually download anything so far. * @param embedder the maven embedder used to read the pomFile * @return a set of artifacts containing only SNAPSHOTs */ private Set<Artifact> getSnapshotArtifactsManually(final MavenEmbedder embedder) { final MavenProject mavenProject; try { mavenProject = embedder.readProject(pomFile); } catch (ProjectBuildingException e) { LOG.error("Failed to read maven2 mavenProject", e); return new HashSet<Artifact>(); } // override default repo if needed final ArtifactRepository localRepo; if (localRepoDir != null) { try { localRepo = embedder.createLocalRepository(localRepoDir); } catch (ComponentLookupException e) { LOG.error("Error setting maven2 local repo to: " + localRepoDir.getAbsolutePath(), e); throw new RuntimeException("Error setting maven2 local repo to: " + localRepoDir.getAbsolutePath() + "; " + e.getMessage()); } } else { localRepo = embedder.getLocalRepository(); } // get snapshot dependencies final Set<Artifact> snapshotArtifacts = getSnapshotDepsManually(embedder, mavenProject); for (final Artifact artifact : snapshotArtifacts) { LOG.debug("Manually examining artifact: " + artifact); resolveArtifact(embedder, artifact, mavenProject, localRepo); } return snapshotArtifacts; } private static Set<Artifact> getSnapshotDepsManually(final MavenEmbedder mavenEmbedder, final MavenProject mavenProject) { final Set<Artifact> retVal = new HashSet<Artifact>(); // Not really sure if mavenEmbedder.readProject() is any better than mavenEmbedder.readModel() // At this point, which ever is used, it should not update files in the local repo. /* //final Set deps = mavenProject.getDependencyArtifacts(); // This returns null, how to init embedder correctly? final List depsList = mavenProject.getDependencyManagement().getSnapshotDepsManually(); //*/ //* // should not update files in the local repo. final Model model = mavenProject.getModel(); final List depsList = model.getDependencies(); //*/ LOG.debug("found dependencies manually: " + depsList.toString()); for (Object aDepsList : depsList) { final Dependency dep = (Dependency) aDepsList; if (dep.getVersion().endsWith(Artifact.SNAPSHOT_VERSION)) { final Artifact artifact; if (dep.getClassifier() != null) { artifact = mavenEmbedder.createArtifactWithClassifier(dep.getGroupId(), dep.getArtifactId(), dep.getVersion(), dep.getType(), dep.getClassifier()); } else { artifact = mavenEmbedder.createArtifact(dep.getGroupId(), dep.getArtifactId(), dep.getVersion(), dep.getScope(), dep.getType()); } if (Artifact.SCOPE_SYSTEM.equals(artifact.getScope())) { // fill in systemPath for file artifact.setFile(new File(dep.getSystemPath(), artifact.getArtifactId() + "-" + artifact.getVersion() + (artifact.getClassifier() != null ? "-" + artifact.getClassifier() : "") + "." + artifact.getType())); } retVal.add(artifact); } } return retVal; } /** * Check for newer timestamps, add modification if change detected. * @param artifactInfo artifact data to be compared against the last build date to determine if modified * @param lastBuild the last build date */ private void checkFile(final ArtifactInfo artifactInfo, long lastBuild) { final File file = artifactInfo.localRepoFile; LOG.debug("Checking artifact: " + artifactInfo.getArtifact()); if ((!file.isDirectory()) && (file.lastModified() > lastBuild)) { addRevision(file, "change", artifactInfo.artifactType + COMMENT_TIMESTAMP_CHANGE + artifactInfo.getArtifact().getArtifactId()); } else if (!file.isDirectory() && !file.exists()) { addRevision(file, "missing", artifactInfo.artifactType + COMMENT_MISSING_IN_LOCALREPO + artifactInfo.getArtifact().getArtifactId()); } } private MavenEmbedder getMvnEmbedder() { final MavenEmbedder mvnEmbedder = new MavenEmbedder(); final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); mvnEmbedder.setClassLoader(classLoader); mvnEmbedder.setLogger(new MavenEmbedderConsoleLogger()); // what do these really do? //mvnEmbedder.setOffline(true); //mvnEmbedder.setCheckLatestPluginVersion(false); //mvnEmbedder.setUpdateSnapshots(false); //mvnEmbedder.setInteractiveMode(false); //mvnEmbedder.setUsePluginRegistry(false); //mvnEmbedder.setPluginUpdateOverride(true); if (localRepoDir != null) { mvnEmbedder.setLocalRepositoryDirectory(localRepoDir); mvnEmbedder.setAlignWithUserInstallation(false); } else { mvnEmbedder.setAlignWithUserInstallation(true); } try { // embedder start can take a long time when debugging mvnEmbedder.start(); } catch (MavenEmbedderException e) { LOG.error("Failed to start embedded maven2", e); } return mvnEmbedder; } }