/******************************************************************************** * 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 hudson.plugins.mavensnapshottrigger; 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.Properties; import java.util.logging.Logger; import java.util.logging.Level; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.Namespace; import org.jdom.input.SAXBuilder; /** * Detects Maven 1.x SNAPSHOT type dependencies which have changed since the last build. * <p> * Idea: "If there is a newer SNAPSHOT dependency file in the <b>local</b> * repository than the latest build, then trigger a build". * <p> * This logic does not detect changes in remote repositories automatically. * However, usually builds happen frequently and thus Maven automatically * downloads new dependencies during every build. * <p> * This implementation is based on MavenSnapshotDependency plugin found in the * CruiseControl distribution. Original code by Tim Shadel. * * @author Jarkko Viinam�ki */ public class MavenSnapshotScanner { /** enable logging for this class */ private static Logger log = Logger.getLogger(MavenSnapshotScanner.class.getName()); /** a collection of File objects which point to modified SNAPSHOT files */ private List<File> modifications; /** Maven POM (project.xml) for the project to be scanned */ private File projectFile; /** Pointer to the local Maven repository (contains JARs etc) */ private File localRepository = new File(System.getProperty("user.home") + "/.maven/repository/"); /** * Sets a full path to the primary project.xml file we are going to scan. */ public void setProjectFile(String s) { projectFile = new File(s); } /** * Set the path for the local Maven repository. * * Properties settings with "maven.local.repo" override this setting. * * @param s full path to the local repository */ public void setLocalRepository(String s) { if( s != null ) { localRepository = new File(s); } } /** * Finds out modified SNAPSHOT dependencies. * * Note! It is possible that this system misses some dependency changes * if snapshots are updated very frequently (i.e. during the build). * * @param lastBuild time when the last build occurred * @return list of File objects that point to SNAPSHOT dependencies that are * newer than the lastBuild instance */ public List<File> getModifications(Date lastBuild) { modifications = new ArrayList<File>(); long lastBuildTime = lastBuild.getTime(); List<String> filenames = new ArrayList<String>(); getSnapshotFilenames(filenames, projectFile, new ArrayList()); for (Iterator<String> itr = filenames.iterator(); itr.hasNext();) { String filename = itr.next(); File dependency = new File(filename); checkFile(dependency, lastBuildTime); } return modifications; } /** Check for newer timestamps */ private void checkFile(File file, long lastBuild) { if (!file.exists()) { log.warning("Dependency not found on disk: " + file.getName()); } else if ((!file.isDirectory()) && (file.lastModified() > lastBuild)) { modifications.add(file); log.fine("Modification detected in " + file.getName()); } } /** * Parses the Maven project file and finds SNAPSHOT dependencies. * * @param filenames this object is used to collect the list of SNAPSHOTs * @param mavenFile Maven project.xml file to parse * @param callstack Maven POM files already processed (prevents cyclic * dependencies) */ void getSnapshotFilenames(List<String> filenames, File mavenFile, List callstack) { log.fine("Getting a list of dependencies for " + mavenFile); Element mavenElement; SAXBuilder builder = new SAXBuilder("org.apache.xerces.parsers.SAXParser"); try { mavenElement = builder.build(mavenFile).getRootElement(); } catch (JDOMException e) { log.log(Level.SEVERE, "failed to load project file [" + (mavenFile != null ? mavenFile.getAbsolutePath() : "") + "]", e); return; } catch (IOException e) { log.severe("failed to load project file [" + (mavenFile != null ? mavenFile.getAbsolutePath() : "") + "]"); return; } Namespace ns = mavenElement.getNamespace(); Properties projectProperties = new Properties(); projectProperties.put("basedir", mavenFile.getParent()); // set some default properties String tmp = mavenElement.getChildText("currentVersion", ns); if (tmp != null) { projectProperties.put("pom.currentVersion", tmp); } // load Maven properties files // http://maven.apache.org/maven-1.x/reference/properties.html /** * 1. Built-in properties are processed 2. ${basedir}/project.properties * (basedir is replaced by the directory where the project.xml file in * use resides) 3. ${basedir}/build.properties 4. * ${user.home}/build.properties 5. System properties */ loadProperties(projectProperties, new File(mavenFile.getParent() + "/project.properties")); loadProperties(projectProperties, new File(mavenFile.getParent() + "/build.properties")); loadProperties(projectProperties, new File(System.getProperty("user.home") + "/build.properties")); // see if this POM extends a parent POM - if so, first parse the parent // TODO: for some unknown reason JDOM/Xerces/SAXParser automatically // seems to(?) transform ${basedir} substring inside the extend tag into // CWD (which is not what we want since basedir should point to // mavenFile dir String extend = mavenElement.getChildTextNormalize("extend", ns); if (extend != null) { String parent = replaceVariables(projectProperties, extend); File parentFile; // first try relative path parentFile = new File(mavenFile.getParent() + "/" + parent); if (!parentFile.exists()) { parentFile = new File(parent); } if (!parentFile.exists()) { log.warning("Could not read parent POM! Invalid extend setting: " + extend); } else if (parentFile.equals(mavenFile)) { log.severe("POM extend tag points to itself!"); } else if (callstack.contains(parentFile)) { log.severe("Cyclic POM inheritance loop detected! Parent POM already processed!"); } else { callstack.add(mavenFile); getSnapshotFilenames(filenames, parentFile, callstack); } } Element depsRoot = mavenElement.getChild("dependencies", ns); // No dependencies listed at all if (depsRoot == null) { log.fine("Project descriptor "+mavenFile+" contains no dependencies!"); return; } // JAR overrides are currently not implemented. Some guidelines how to // do it: // http://jira.public.thoughtworks.org/browse/CC-141 // http://maven.apache.org/maven-1.x/using/managing-dependencies.html /* * boolean mavenJarOverride = false; * * String tmp = projectProperties.getProperty("maven.jar.override"); if * (tmp != null && (tmp.equalsIgnoreCase("on") || * tmp.equalsIgnoreCase("true"))) { mavenJarOverride = true; } */ List dependencies = depsRoot.getChildren(); File localRepo = localRepository; // allow projects and properties settings to override local repo if( projectProperties.containsKey("maven.repo.local") ) { localRepo = new File(projectProperties.getProperty("maven.repo.local")); } Iterator itr = dependencies.iterator(); while (itr.hasNext()) { Element dependency = (Element) itr.next(); String versionText = dependency.getChildText("version", ns); if (versionText == null) { continue; } // versionText may also include ${pom.currentVersion} and if // project/currentVersion is of type xxx-SNAPSHOT, we need to // include // that dependency versionText = replaceVariables(projectProperties, versionText); // "the version need only contain the word SNAPSHOT - it does // not need to equal it exactly." // @see // http://maven.apache.org/maven-1.x/using/managing-dependencies.html if (versionText.indexOf("SNAPSHOT") != -1) { 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); if (type == null) { type = "jar"; } // Repository path format: // ${repo}/${groupId}/${type}s/${artifactId}-${version}.${type} StringBuffer fileName = new StringBuffer(); fileName.append(localRepo.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.fine("Snapshot detected: " + fileName); filenames.add(file.getAbsolutePath()); } } } void loadProperties(Properties properties, File file) { if (file.exists()) { BufferedInputStream in = null; try { FileInputStream fin = new FileInputStream(file); in = new BufferedInputStream(fin); properties.load(in); log.fine("Loaded " + file.getAbsolutePath()); } catch (IOException ex) { log.log(Level.SEVERE, "failed to load project properties file [" + file.getAbsolutePath() + "]", ex); } finally { try { in.close(); } catch (Exception ex) { // we don't care } } } } /** * 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); 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; } i = value.indexOf("${", pos); } return value; } }