/*
* Copyright (C) 2011 JFrog Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jfrog.build.extractor.maven.transformer;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.model.Model;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.jfrog.build.extractor.EolDetectingInputStream;
import org.jfrog.build.extractor.maven.reader.ModuleName;
import java.io.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
/**
* Rewrites the project versions in the pom.
*
* @author Yossi Shaul
*/
public class PomTransformer {
private final String scmUrl;
private final ModuleName currentModule;
private final Map<ModuleName, String> versionsByModule;
private final boolean failOnSnapshot;
private final boolean dryRun;
private boolean modified;
private File pomFile;
private Map<String, String> pomProperties = new HashMap<String, String>();
private String nextPomToLoad = null;
/**
* Transforms single pom file.
*
* @param currentModule The current module we work on
* @param versionsByModule Map of module names to module version
* @param scmUrl Scm url to use if scm element exists in the pom file
*/
public PomTransformer(ModuleName currentModule, Map<ModuleName, String> versionsByModule, String scmUrl) {
this(currentModule, versionsByModule, scmUrl, false, false);
}
/**
* Transforms single pom file.
*
* @param currentModule The current module we work on
* @param versionsByModule Map of module names to module version
* @param scmUrl Scm url to use if scm element exists in the pom file
* @param failOnSnapshot If true, fail with IllegalStateException if the pom contains snapshot version after the
* version changes
*/
public PomTransformer(ModuleName currentModule, Map<ModuleName, String> versionsByModule, String scmUrl,
boolean failOnSnapshot) {
this(currentModule, versionsByModule, scmUrl, failOnSnapshot, false);
}
/**
* Transforms single pom file.
*
* @param currentModule The current module we work on
* @param versionsByModule Map of module names to module version
* @param scmUrl Scm url to use if scm element exists in the pom file
* @param failOnSnapshot If true, fail with IllegalStateException if the pom contains snapshot version after the
* version changes
* @param dryRun If true, changes will not take effect.
*
*/
public PomTransformer(ModuleName currentModule, Map<ModuleName, String> versionsByModule, String scmUrl,
boolean failOnSnapshot, boolean dryRun) {
this.currentModule = currentModule;
this.versionsByModule = versionsByModule;
this.scmUrl = scmUrl;
this.failOnSnapshot = failOnSnapshot;
this.dryRun = dryRun;
}
/**
* Performs the transformation.
*
* @return True if the file was modified.
*/
public Boolean transform(File pomFile) throws IOException {
this.pomFile = pomFile;
if (!pomFile.exists()) {
throw new IllegalArgumentException("Couldn't find pom file: " + pomFile);
}
SAXBuilder saxBuilder = createSaxBuilder();
Document document;
EolDetectingInputStream eolDetectingStream = null;
InputStreamReader inputStreamReader = null;
try {
eolDetectingStream = new EolDetectingInputStream(new FileInputStream(pomFile));
inputStreamReader = new InputStreamReader(eolDetectingStream, "UTF-8");
document = saxBuilder.build(inputStreamReader);
} catch (JDOMException e) {
throw new IOException("Failed to parse pom: " + pomFile.getAbsolutePath(), e);
} finally {
IOUtils.closeQuietly(inputStreamReader);
IOUtils.closeQuietly(eolDetectingStream);
}
Element rootElement = document.getRootElement();
Namespace ns = rootElement.getNamespace();
getProperties(rootElement, ns);
changeParentVersion(rootElement, ns);
changeCurrentModuleVersion(rootElement, ns);
//changePropertiesVersion(rootElement, ns);
changeDependencyManagementVersions(rootElement, ns);
changeDependencyVersions(rootElement, ns);
if (scmUrl != null) {
changeScm(rootElement, ns);
}
if (modified && !dryRun) {
FileOutputStream fileOutputStream = new FileOutputStream(pomFile);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "UTF-8");
try {
XMLOutputter outputter = new XMLOutputter();
String eol = eolDetectingStream.getEol();
if (!"".equals(eol)) {
Format format = outputter.getFormat();
format.setLineSeparator(eol);
format.setTextMode(Format.TextMode.PRESERVE);
outputter.setFormat(format);
}
outputter.output(document, outputStreamWriter);
} finally {
IOUtils.closeQuietly(outputStreamWriter);
IOUtils.closeQuietly(fileOutputStream);
}
}
return modified;
}
private void getProperties(Element root, Namespace ns) {
Element propertiesElement = root.getChild("properties", ns);
if (propertiesElement == null) {
return;
}
List<Element> children = propertiesElement.getChildren();
for(Element child : children) {
String key = child.getName();
if(pomProperties.get(key) == null) {
pomProperties.put(key, child.getText());
}
}
}
private void changeParentVersion(Element root, Namespace ns) {
Element parentElement = root.getChild("parent", ns);
if (parentElement == null) {
return;
}
// We might need to use the parent pom for getting properties
// If the file has parent but relativePath is not set we use the default parent location
Element relativePath = parentElement.getChild("relativePath", ns);
String relativeParentPath = relativePath == null ? ".." + java.io.File.separator + "pom.xml" : relativePath.getText();
nextPomToLoad = StringUtils.substringBeforeLast(pomFile.getAbsolutePath(), java.io.File.separator) + java.io.File.separator + relativeParentPath;
ModuleName parentName = extractModuleName(parentElement, ns);
if (versionsByModule.containsKey(parentName)) {
setVersion(parentElement, ns, versionsByModule.get(parentName));
}
verifyNonSnapshotVersion(parentName, parentElement, ns);
}
private void changeCurrentModuleVersion(Element rootElement, Namespace ns) {
setVersion(rootElement, ns, versionsByModule.get(currentModule));
verifyNonSnapshotVersion(currentModule, rootElement, ns);
}
private void changeDependencyManagementVersions(Element rootElement, Namespace ns) {
Element dependencyManagement = rootElement.getChild("dependencyManagement", ns);
if (dependencyManagement == null) {
return;
}
Element dependenciesElement = dependencyManagement.getChild("dependencies", ns);
if (dependenciesElement == null) {
return;
}
List<Element> dependencies = dependenciesElement.getChildren("dependency", ns);
for (Element dependency : dependencies) {
changeDependencyVersion(ns, dependency);
}
}
private void changeDependencyVersions(Element rootElement, Namespace ns) {
Element dependenciesElement = rootElement.getChild("dependencies", ns);
if (dependenciesElement == null) {
return;
}
List<Element> dependencies = dependenciesElement.getChildren("dependency", ns);
for (Element dependency : dependencies) {
changeDependencyVersion(ns, dependency);
}
}
private void changeDependencyVersion(Namespace ns, Element dependency) {
ModuleName moduleName = extractModuleName(dependency, ns);
if (versionsByModule.containsKey(moduleName)) {
setVersion(dependency, ns, versionsByModule.get(moduleName));
}
verifyNonSnapshotVersion(moduleName, dependency, ns);
}
private void changeScm(Element rootElement, Namespace ns) {
Element scm = rootElement.getChild("scm", ns);
if (scm == null) {
return;
}
Element connection = scm.getChild("connection", ns);
if (connection != null) {
connection.setText("scm:svn:" + scmUrl);
}
Element developerConnection = scm.getChild("developerConnection", ns);
if (developerConnection != null) {
developerConnection.setText("scm:svn:" + scmUrl);
}
Element url = scm.getChild("url", ns);
if (url != null) {
url.setText(scmUrl);
}
}
private void setVersion(Element element, Namespace ns, String version) {
Element versionElement = element.getChild("version", ns);
if (versionElement != null) {
String currentVersion = versionElement.getText();
if (!version.equals(currentVersion)) {
versionElement.setText(version);
modified = true;
}
}
}
private void verifyNonSnapshotVersion(ModuleName moduleName, Element element, Namespace ns) {
if (!failOnSnapshot) {
return;
}
Element versionElement = element.getChild("version", ns);
if (versionElement != null) {
String currentVersion = versionElement.getText();
if (currentVersion.endsWith("-SNAPSHOT") ||
(currentVersion.startsWith("${") && currentVersion.endsWith("}")) && evalExpression(currentVersion) != null && evalExpression(currentVersion).endsWith("-SNAPSHOT")) {
throw new SnapshotNotAllowedException(String.format("Snapshot detected in file '%s': %s:%s",
pomFile.getAbsolutePath(), moduleName, currentVersion));
}
}
}
// Search for expression value in the pom properties
// If not found, try to find the value up in the pom hierarchy, lazy loading
private String evalExpression(String expr) {
String expression = pomProperties.get(expr.substring(2, expr.lastIndexOf("}")));
if (expression != null) {
return expression;
}
if (loadNextProperties()) {
return evalExpression(expr);
}
return null;
}
// The nextPomToLoad point to the next pom hierarchy
// load the next pom in chain and his properties
private boolean loadNextProperties() {
if (nextPomToLoad == null) {
return false;
}
Model model;
FileReader reader = null;
MavenXpp3Reader mavenReader = new MavenXpp3Reader();
try {
reader = new FileReader(nextPomToLoad);
model = mavenReader.read(reader);
Properties properties = model.getProperties();
for (String key : properties.stringPropertyNames()) {
if (pomProperties.get(key) == null) {
pomProperties.put(key, properties.getProperty(key));
}
}
if (model.getParent() != null) {
nextPomToLoad = StringUtils.substringBeforeLast(nextPomToLoad, java.io.File.separator) + java.io.File.separator + model.getParent().getRelativePath();
} else {
nextPomToLoad = null;
}
} catch (Exception e) {
Logger.getLogger(PomTransformer.class.getName()).info("couldn't load pom file at: " + nextPomToLoad);
return false;
} finally {
IOUtils.closeQuietly(reader);
}
return true;
}
private ModuleName extractModuleName(Element element, Namespace ns) {
String groupId = element.getChildText("groupId", ns);
String artifactId = element.getChildText("artifactId", ns);
if (StringUtils.isBlank(groupId) || StringUtils.isBlank(artifactId)) {
throw new IllegalArgumentException("Couldn't extract module key from: " + element);
}
return new ModuleName(groupId, artifactId);
}
static SAXBuilder createSaxBuilder() {
SAXBuilder sb = new SAXBuilder();
// don't validate and don't load dtd
sb.setValidation(false);
sb.setFeature("http://xml.org/sax/features/validation", false);
sb.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
sb.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
return sb;
}
}