/******************************************************************************** * 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.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.Modification; import net.sourceforge.cruisecontrol.SourceControl; import net.sourceforge.cruisecontrol.util.ValidationHelper; import net.sourceforge.cruisecontrol.util.IO; import org.apache.log4j.Logger; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.Namespace; import org.jdom.input.SAXBuilder; /** * Checks binary dependencies listed in a Maven project rather than in a * repository. * * <pre> * Modifications 20060626 (jarkko.viinamaki at removethis.tietoenator.com): * - made POM scanning namespace aware. Dependencies were not detected if project.xml * had schema definition in the project element * - added support for "ejb-client" dependency type * - added echo for detected snapshot dependencies * - added support for build.properties or other similiar properties file which contains * key=value tags to replace ${key} type strings in project.xml * Modifications 20060627 * - fixed a bug in replaceVariables method * </pre> * * @author Tim Shadel */ public class MavenSnapshotDependency implements SourceControl { private SourceControlProperties properties = new SourceControlProperties(); private List<Modification> modifications; private File projectFile; private File propertiesFile; private File localRepository = new File(System.getProperty("user.home") + "/.maven/repository/"); private String user; /** enable logging for this class */ private static final Logger LOG = Logger.getLogger(MavenSnapshotDependency.class); /** * Set the root folder of the directories that we are going to scan */ public void setProjectFile(String s) { projectFile = new File(s); } /** * Sets the .properties file which contains overriding tags for POM. * * Default is build.properties */ public void setPropertiesFile(String s) { propertiesFile = new File(s); } /** * Set the path for the local Maven repository */ public void setLocalRepository(String s) { localRepository = new File(s); } /** * Set the username listed with changes found in binary dependencies */ public void setUser(String s) { user = s; } public void setProperty(String property) { properties.assignPropertyName(property); } public Map<String, String> getProperties() { return properties.getPropertiesAndReset(); } public void validate() throws CruiseControlException { ValidationHelper.assertIsSet(projectFile, "projectFile", this.getClass()); ValidationHelper.assertTrue(projectFile.exists(), "Project file '" + projectFile.getAbsolutePath() + "' does not exist."); ValidationHelper.assertFalse(projectFile.isDirectory(), "The directory '" + projectFile.getAbsolutePath() + "' cannot be used as the projectFile for MavenSnapshotDependency."); ValidationHelper.assertTrue(localRepository.exists(), "Local Maven repository '" + localRepository.getAbsolutePath() + "' does not exist."); ValidationHelper.assertTrue(localRepository.isDirectory(), "Local Maven repository '" + localRepository.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(Date lastBuild, Date now) { modifications = new ArrayList<Modification>(); checkProjectDependencies(projectFile, lastBuild.getTime()); return modifications; } /** * Add a Modification to the list of modifications. All modifications are * listed as "change" and all have the same comment. */ private void addRevision(File dependency) { Modification mod = new Modification("maven"); Modification.ModifiedFile modfile = mod.createModifiedFile(dependency.getName(), dependency.getParent()); modfile.action = "change"; mod.userName = user; mod.modifiedTime = new Date(dependency.lastModified()); mod.comment = "Maven project dependency: timestamp change detected."; modifications.add(mod); properties.modificationFound(); } /** * Use Maven library to open project file and check for newer dependencies. * Do not download them, only list them as modifications. */ private void checkProjectDependencies(File projectFile, long lastBuild) { final List filenames = getSnapshotFilenames(projectFile); final Iterator itr = filenames.iterator(); while (itr.hasNext()) { String filename = (String) itr.next(); File dependency = new File(filename); checkFile(dependency, lastBuild); } } /** * Replaces variables in a string defined as ${key}. * * Values for variables are taken from given properties or System properties. * Replacement is recursive. If ${key} maps to a string which has other ${keyN} values, * those ${keyN} values are replaced also if there is a matching value for them. */ String replaceVariables(Properties p, String value) { if (value == null || p == null) { return value; } int i = value.indexOf("${"); if (i == -1) { return value; } int pos = 0; while (i != -1) { int j = value.indexOf("}", i); if (j == -1) { break; } String key = value.substring(i + 2, j); // LOG.info("Tag: " + key); if (p.containsKey(key)) { value = value.substring(0, i) + p.getProperty(key) + value.substring(j + 1); // step one forward from ${ position, otherwise we can get an infinite loop pos = i + 1; } else if (System.getProperty(key) != null) { value = value.substring(0, i) + System.getProperty(key) + value.substring(j + 1); pos = i + 1; } else { // could not replace the value, leave it there pos = j + 1; } // LOG.info("New value: " + value); i = value.indexOf("${", pos); } return value; } /** * Parse the Maven project file, and file names */ List getSnapshotFilenames(File mavenFile) { LOG.info("Getting a list of dependencies for " + mavenFile); final List<String> filenames = new ArrayList<String>(); Element mavenElement; SAXBuilder builder = new SAXBuilder("org.apache.xerces.parsers.SAXParser"); try { mavenElement = builder.build(mavenFile).getRootElement(); } catch (JDOMException e) { LOG.error("failed to load project file [" + (mavenFile != null ? mavenFile.getAbsolutePath() : "") + "]", e); return filenames; } catch (IOException e) { LOG.error("failed to load project file [" + (mavenFile != null ? mavenFile.getAbsolutePath() : "") + "]", e); return filenames; } // load the project properties file if it exists Properties projectProperties = new Properties(); if (propertiesFile == null) { propertiesFile = new File(mavenFile.getParent() + "/build.properties"); } if (propertiesFile.exists()) { BufferedInputStream in = null; try { FileInputStream fin = new FileInputStream(propertiesFile); in = new BufferedInputStream(fin); projectProperties.load(in); } catch (IOException ex) { LOG.error("failed to load project properties file [" + propertiesFile.getAbsolutePath() + "]", ex); } finally { IO.close(in); } } // set some default properties projectProperties.put("basedir", mavenFile.getParent()); // JAR overrides are currently not implemented. Some guidelines how to do it: // http://jira.public.thoughtworks.org/browse/CC-141 /* boolean mavenJarOverride = false; String tmp = projectProperties.getProperty("maven.jar.override"); if (tmp != null && (tmp.equalsIgnoreCase("on") || tmp.equalsIgnoreCase("true"))) { mavenJarOverride = true; } */ Namespace ns = mavenElement.getNamespace(); Element depsRoot = mavenElement.getChild("dependencies", ns); // No dependencies listed at all if (depsRoot == null) { LOG.warn("No dependencies detected."); return filenames; } List dependencies = depsRoot.getChildren(); Iterator itr = dependencies.iterator(); while (itr.hasNext()) { Element dependency = (Element) itr.next(); String versionText = dependency.getChildText("version", ns); if (versionText != null && versionText.endsWith("SNAPSHOT")) { String groupId = dependency.getChildText("groupId", ns); String artifactId = dependency.getChildText("artifactId", ns); String id = dependency.getChildText("id", ns); String type = dependency.getChildText("type", ns); // replace variables artifactId = replaceVariables(projectProperties, artifactId); groupId = replaceVariables(projectProperties, groupId); id = replaceVariables(projectProperties, id); versionText = replaceVariables(projectProperties, versionText); if (type == null) { type = "jar"; } // Format: // ${repo}/${groupId}/${type}s/${artifactId}-${version}.${type} StringBuffer fileName = new StringBuffer(); fileName.append(localRepository.getAbsolutePath()); fileName.append('/'); if (groupId != null) { fileName.append(groupId); } else { fileName.append(id); } fileName.append('/'); if ("ejb-client".equals(type)) { fileName.append("ejb"); } else { fileName.append(type); } fileName.append('s'); fileName.append('/'); if (artifactId != null) { fileName.append(artifactId); } else { fileName.append(id); } fileName.append('-'); fileName.append(versionText); if ("ejb-client".equals(type)) { fileName.append("-client"); } fileName.append('.'); if ("uberjar".equals(type) || "ejb".equals(type) || "plugin".equals(type) || "ejb-client".equals(type)) { fileName.append("jar"); } else { fileName.append(type); } File file = new File(fileName.toString()); LOG.info("Snapshot detected: " + fileName); filenames.add(file.getAbsolutePath()); } } return filenames; } /** Check for newer timestamps */ private void checkFile(File file, long lastBuild) { if ((!file.isDirectory()) && (file.lastModified() > lastBuild)) { addRevision(file); } } }