/* * Copyright PMEase Inc., * Date: Feb 25, 2008 * Time: 10:26:02 AM * All rights reserved. * * Revision: $Id: MavenBuildStep.java 1551 2008-10-24 08:27:46Z robin $ */ package com.pmease.quickbuild.plugin.builder.maven; import java.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Stack; import org.apache.commons.lang.SystemUtils; import org.apache.commons.lang.math.NumberUtils; import org.apache.tools.ant.types.Environment; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.hibernate.validator.constraints.NotEmpty; import com.pmease.quickbuild.Context; import com.pmease.quickbuild.Property; import com.pmease.quickbuild.QuickbuildException; import com.pmease.quickbuild.annotation.Advanced; import com.pmease.quickbuild.annotation.Editable; import com.pmease.quickbuild.annotation.ScriptApi; import com.pmease.quickbuild.annotation.Scriptable; import com.pmease.quickbuild.execution.Commandline; import com.pmease.quickbuild.execution.LineConsumer; import com.pmease.quickbuild.migration.VersionedDocument; import com.pmease.quickbuild.stepsupport.Step; import com.pmease.quickbuild.util.FileUtils; import com.pmease.quickbuild.util.StringUtils; /** * @author robin */ @Editable(name="Maven", category="Build", description= "Configure a Maven based build step here. By default, QuickBuild executes \"mvn\" " + "(or mvn.bat on Windows) to run this build step, and expects this file to be on " + "the system path. If not, you will need to specify path to this file by configuring " + "the Maven plugin through the plugin management page.") @ScriptApi("This step calls Apache Maven to build projects.") public class MavenBuildStep extends Step { private static final long serialVersionUID = 1L; private String workingPath; private String buildFile; private String goals; private List buildProperties = new ArrayList(); private String extraMavenOptions; private List environments = new ArrayList(); private boolean syncBuildVersion = true; private int returnCode; @Editable(order=1100, name="Working Directory", description= "Specify working directory of the Maven command. A non-absolute path is considered " + "to be relative to the workspace directory. If not specified, the workspace " + "directory will be used as working directory." ) @ScriptApi("Get working directory of the Maven command. Null if the " + "workspace directory is used as working directory.") @Scriptable public String getWorkingPath() { return workingPath; } @Editable(order=1150, name="Build File", description= "Specify name of the Maven build file. If not specified, the default Maven build " + "file name will be used." ) @ScriptApi("Get Maven build file name.") @Scriptable public String getBuildFile() { return buildFile; } public void setBuildFile(String buildFile) { this.buildFile = buildFile; } public void setWorkingPath(String workingPath) { this.workingPath = workingPath; } @Editable(order=1200, description= "Specify the goals to build. Use space to separate different goals " + "(goal name containing spaces should be quoted in order not to be " + "interpreted as multiple goals).") @NotEmpty @ScriptApi("Get Maven goals. Null if not specified.") @Scriptable public String getGoals() { return goals; } public void setGoals(String goals) { this.goals = goals; } @Editable(order=1250, description="If checked, build version will be set to version found in the POM.
" + "NOTE: This option only takes effect if the Resolve Effective POM option is enabled in " + "Maven plugin setting in plugin management page.") public boolean isSyncBuildVersion() { return syncBuildVersion; } public void setSyncBuildVersion(boolean syncBuildVersion) { this.syncBuildVersion = syncBuildVersion; } @Editable(order=1300, description= "Define build properties here to pass into Maven. For example, " + "you may pass version of current build as property buildVersion " + "by specifying property name as buildVersion and property value as " + "${build.version}.
" + "NOTE: Properties with blank value will be ignored." ) @ScriptApi("Get list of build properties passed to Maven.") public List getBuildProperties() { return buildProperties; } public void setBuildProperties(List buildProperties) { this.buildProperties = buildProperties; } @Editable(order=1500, description= "Optionally specify extra Maven options. If specified, it will be appended to the maven " + "global options specified in maven plugin setting. Please note that you should NOT " + "specify below options as they'll be determined by QuickBuild:
" + "-X, --debug, -q, --quiet, -D, --define") @ScriptApi("Get extra Maven build options. Null if not specified.") @Scriptable @Advanced public String getExtraMavenOptions() { return extraMavenOptions; } public void setExtraMavenOptions(String extraMavenOptions) { this.extraMavenOptions = extraMavenOptions; } /** * Get defined environment variables for this build step. */ @Editable(name="Environment Variables", order=1600, description= "Specify environment variables for Maven execution. For example, " + "you may store version of current build into environment variable buildVersion " + "by specifying variable name as buildVersion and variable value as " + "${build.version}.
" + "NOTE: " + "
  • You may define environment variable JAVA_HOME here in order to build " + "with your choosed JDK." + "
  • Environment variables with blank value will be ignored.
" ) @ScriptApi("Get defined environment variables when executing the build command.") public List getEnvironments() { return environments; } public void setEnvironments(List environments) { this.environments= environments; } @ScriptApi("Get return code of Maven command.") public int getReturnCode() { return returnCode; } private Commandline getBuildCmd(MavenSetting mavenSetting, boolean resolveEffectivePOM) { Commandline cmdline = new Commandline(); cmdline.setExecutable(getMavenExecutable(mavenSetting)); String globalOpts = mavenSetting.getMavenOptions(); if (StringUtils.isNotBlank(globalOpts)) cmdline.createArgument().setLine(globalOpts); if (StringUtils.isNotBlank(getExtraMavenOptions())) cmdline.createArgument().setLine(getExtraMavenOptions()); if (StringUtils.isNotBlank(getBuildFile())) cmdline.createArgument().setValue("-f" + getBuildFile()); for (Property property: getBuildProperties()) { if (StringUtils.isNotBlank(property.getValue())) { cmdline.createArgument().setValue("-D" + property.getName() + "=" + property.getValue()); } } if (!resolveEffectivePOM) { // set maven log level based on project's log level if log level does not // been explicitely specified in maven command String cmdLineDescription = cmdline.describe(); if (!cmdLineDescription.matches(".*\\s(-X|--debug)($|\\s.*)")) { if (Context.getLogger().isDebugEnabled()) cmdline.createArgument().setValue("-X"); } if (StringUtils.isNotBlank(getGoals())) cmdline.createArgument().setLine(getGoals()); } else { cmdline.createArgument().setLine("help:effective-pom"); } return cmdline; } private File getWorkingDir() { return FileUtils.resolvePath(Context.getConfiguration().getWorkspaceDir(), getWorkingPath()); } private String getMavenExecutable(MavenSetting mvnSetting) { String mvnExe = mvnSetting.getMavenExePath(); if (mvnExe == null) { String mvnHome = mvnSetting.getMavenHome(); if (mvnHome == null) { if (SystemUtils.IS_OS_WINDOWS) mvnExe = "mvn.bat"; else mvnExe = "mvn"; } else { mvnHome = StringUtils.stripEnd(mvnHome, "/\\"); if (SystemUtils.IS_OS_WINDOWS) mvnExe = mvnHome + "\\bin\\mvn.bat"; else mvnExe = mvnHome + "/bin/mvn"; } } return mvnExe; } @SuppressWarnings("unchecked") @Override public void run() { MavenSetting mavenSetting = ((MavenSetting) getPlugin().getSetting(true)); Environment env = new Environment(); String mvnHome = mavenSetting.getMavenHome(); if (mvnHome != null) { Environment.Variable var = new Environment.Variable(); var.setKey("M2_HOME"); var.setValue(mvnHome); env.addVariable(var); } for (Property each: getActualEnvironments(getEnvironments())) { if (StringUtils.isNotBlank(each.getValue())) { Environment.Variable var = new Environment.Variable(); var.setKey(each.getName()); var.setValue(each.getValue()); env.addVariable(var); } } final StringBuffer errorMessages = new StringBuffer(); try { Commandline cmdline = getBuildCmd(mavenSetting, false); final boolean[] successful = new boolean[]{false}; final String[] invokeDirError = new String[]{null}; // If MAVEN_BATCH_PAUSE is set in mvn.bat, user needs to press a key before // reactor goal finishes. This command inputs simulates that key stroke. Commandline.ExecuteResult result = cmdline.execute(getWorkingDir(), env, new LineConsumer() { private boolean inErrorBlock = false; @Override public void consume(String line) { if (!isDownloadCounter(line)) { if (line.startsWith("[ERROR] ")) { String errorMessage = line.substring("[ERROR] ".length()); Context.getLogger().error(errorMessage); errorMessages.append(errorMessage).append("\n"); inErrorBlock = true; } else if (line.equals("[INFO] BUILD FAILURE")) { Context.getLogger().error(line.substring("[INFO] ".length())); inErrorBlock = false; } else if (line.startsWith("[INFO] ")) { Context.getLogger().info(line.substring("[INFO] ".length())); inErrorBlock = false; } else if (line.startsWith("[DEBUG] ")) { Context.getLogger().debug(line.substring("[DEBUG] ".length())); inErrorBlock = false; } else if (line.startsWith("[WARNING] ")) { Context.getLogger().warn(line.substring("[WARNING] ".length())); inErrorBlock = false; } else if (inErrorBlock){ Context.getLogger().error(line); errorMessages.append(line).append("\n"); } else { Context.getLogger().info(line); } } if (line.contains("BUILD SUCCESSFUL") || line.contains("BUILD SUCCESS")) { successful[0] = true; } else if (line.contains("Please verify you invoked Maven from the correct directory.")) { if (line.startsWith("[ERROR] ")) invokeDirError[0] = line.substring("[ERROR] ".length()); else invokeDirError[0] = line; } } private boolean isDownloadCounter(String logLine) { if (logLine.endsWith("/?")) { // maven 2.x String prefix = logLine.substring(0, logLine.length()-2); return NumberUtils.isDigits(prefix); } else { String temp = null; logLine = logLine.trim().toLowerCase(); if (logLine.endsWith("kb") || logLine.endsWith("mb") || logLine.endsWith("gb")) temp = logLine.substring(0, logLine.length()-2); else if (logLine.endsWith("b") || logLine.endsWith("k") || logLine.endsWith("m") || logLine.endsWith("g")) temp = logLine.substring(0, logLine.length()-1); if (temp != null) { temp = temp.replace('/', '0').replace(' ', '0').replace('\t', '0').replace('k', '0') .replace('b', '0').replace('m', '0').replace('g', '0'); return NumberUtils.isDigits(temp); } else { return false; } } } }, new LineConsumer() { @Override public void consume(String line) { Context.getLogger().warn(line); if (line.contains("BUILD SUCCESSFUL") || line.contains("BUILD SUCCESS")) successful[0] = true; } }, "\n"); returnCode = result.getReturnCode(); if (invokeDirError[0] != null) throw new QuickbuildException(invokeDirError[0]); if (result.getReturnCode() != 0) { if (errorMessages.length() != 0) throw new QuickbuildException(errorMessages.toString().trim()); else result.buildException(); } if (!successful[0]) { if (errorMessages.length() != 0) throw new QuickbuildException(errorMessages.toString().trim()); else throw new QuickbuildException("Maven build failed."); } } finally { if (mavenSetting.isResolveEffectivePOM()) { Commandline cmdline = getBuildCmd(mavenSetting, true); Context.getLogger().info("Checking project gavs and dependency gavs..."); final String[] invokeDirError = new String[]{null}; File tempFile = FileUtils.createTempFile("maven"); try { final StringBuffer checkPOMErrorMessages = new StringBuffer(); cmdline.createArgument().setValue("-Doutput=" + tempFile.getAbsolutePath()); Commandline.ExecuteResult result = cmdline.execute(getWorkingDir(), env, new LineConsumer() { private boolean inErrorBlock = false; @Override public void consume(String line) { if (line.startsWith("[ERROR] ")) { String errorMessage = line.substring("[ERROR] ".length()); Context.getLogger().error(errorMessage); checkPOMErrorMessages.append(errorMessage).append("\n"); inErrorBlock = true; } else if (line.equals("[INFO] BUILD FAILURE")) { Context.getLogger().error(line.substring("[INFO] ".length())); inErrorBlock = false; } else if (line.startsWith("[INFO] ")) { Context.getLogger().debug(line.substring("[INFO] ".length())); inErrorBlock = false; } else if (line.startsWith("[DEBUG] ")) { Context.getLogger().debug(line.substring("[DEBUG] ".length())); inErrorBlock = false; } else if (line.startsWith("[WARNING] ")) { Context.getLogger().warn(line.substring("[WARNING] ".length())); inErrorBlock = false; } else if (inErrorBlock) { Context.getLogger().error(line); checkPOMErrorMessages.append(line).append("\n"); } else { Context.getLogger().debug(line); } if (line.contains("Please verify you invoked Maven from the correct directory.")) { if (line.startsWith("[ERROR] ")) invokeDirError[0] = line.substring("[ERROR] ".length()); else invokeDirError[0] = line; } } }, new LineConsumer.WarnLogger()); if (invokeDirError[0] != null) throw new QuickbuildException(invokeDirError[0]); if (result.getReturnCode() != 0) { if (checkPOMErrorMessages.length() != 0) throw new QuickbuildException(checkPOMErrorMessages.toString().trim()); else result.buildException(); } if (Context.getLogger().isDebugEnabled()) Context.getLogger().debug("Effective POM: \n" + FileUtils.readFileAsString(tempFile)); Set gavs = new HashSet(); Set dependencyGavs = new HashSet(); List projects = new ArrayList(); Document pom; try { pom = new SAXReader().read(tempFile); } catch (DocumentException e) { SAXReader reader = new SAXReader(); reader.setEncoding(FileUtils.getDefaultEncoding()); pom = reader.read(tempFile); } if (pom.getRootElement().getName().equals("project")) projects.add(pom.getRootElement()); else if (pom.getRootElement().getName().equals("projects")) projects.addAll(pom.getRootElement().elements()); else throw new QuickbuildException("Unrecognized pom format"); if (isSyncBuildVersion()) getBuild().setVersion(projects.get(0).elementText("version").trim()); for (Element project: projects) { if (!"pom".equalsIgnoreCase(project.elementTextTrim("packaging"))) { Gav gav = new Gav(project.elementText("groupId").trim(), project.elementText("artifactId").trim(), project.elementText("version").trim()); gavs.add(gav); Element dependenciesElement = project.element("dependencies"); if (dependenciesElement != null) { for (Element dependencyElement: (List)dependenciesElement.elements()) { gav = new Gav(dependencyElement.elementText("groupId").trim(), dependencyElement.elementText("artifactId").trim(), dependencyElement.elementText("version").trim()); dependencyGavs.add(gav); } } } } Set existingGavs = (Set) getBuild().getReports().get(MavenPlugin.GAVS); if (existingGavs != null) gavs.addAll(existingGavs); getBuild().getReports().put(MavenPlugin.GAVS, gavs); Set existingDependencyGavs = (Set) getBuild().getReports().get(MavenPlugin.DEPENDENCY_GAVS); if (existingDependencyGavs != null) dependencyGavs.addAll(existingDependencyGavs); getBuild().getReports().put(MavenPlugin.DEPENDENCY_GAVS, dependencyGavs); } catch (DocumentException e) { throw new RuntimeException(e); } finally { FileUtils.deleteFile(tempFile); } } } } @SuppressWarnings("unused") private void migrate1(VersionedDocument dom, Stack versions) { Element element = dom.getRootElement().element("dirToRunMaven"); if (element != null) { element.detach(); dom.getRootElement().addElement("workingPath").setText(element.getTextTrim()); } } @SuppressWarnings("unused") private void migrate2(VersionedDocument dom, Stack versions) { Element root = dom.getRootElement(); root.element("commandSuccessCondition").detach(); root.element("environmentVariables").setName("environments"); if (!versions.empty()) { versions.pop(); } else { versions.push(0); versions.push(0); } } @SuppressWarnings("unused") private void migrate3(VersionedDocument dom, Stack versions) { dom.getRootElement().addElement("syncBuildVersion").setText("false"); } }