Java tutorial
package org.apache.maven.plugin.invoker; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.apache.commons.io.input.XmlStreamReader; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Model; import org.apache.maven.model.Profile; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecution; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.invoker.model.BuildJob; import org.apache.maven.plugin.invoker.model.io.xpp3.BuildJobXpp3Writer; import org.apache.maven.plugin.registry.TrackableBase; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.maven.settings.RuntimeInfo; import org.apache.maven.settings.Settings; import org.apache.maven.settings.SettingsUtils; import org.apache.maven.settings.io.xpp3.SettingsXpp3Reader; import org.apache.maven.settings.io.xpp3.SettingsXpp3Writer; import org.apache.maven.shared.invoker.CommandLineConfigurationException; import org.apache.maven.shared.invoker.DefaultInvocationRequest; import org.apache.maven.shared.invoker.InvocationRequest; import org.apache.maven.shared.invoker.InvocationResult; import org.apache.maven.shared.invoker.Invoker; import org.apache.maven.shared.invoker.MavenCommandLineBuilder; import org.apache.maven.shared.invoker.MavenInvocationException; import org.apache.maven.shared.scriptinterpreter.RunErrorException; import org.apache.maven.shared.scriptinterpreter.RunFailureException; import org.apache.maven.shared.scriptinterpreter.ScriptRunner; import org.codehaus.plexus.interpolation.InterpolationException; import org.codehaus.plexus.interpolation.Interpolator; import org.codehaus.plexus.interpolation.MapBasedValueSource; import org.codehaus.plexus.interpolation.RegexBasedInterpolator; import org.codehaus.plexus.util.DirectoryScanner; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.InterpolationFilterReader; import org.codehaus.plexus.util.ReaderFactory; import org.codehaus.plexus.util.ReflectionUtils; import org.codehaus.plexus.util.StringUtils; import org.codehaus.plexus.util.WriterFactory; import org.codehaus.plexus.util.cli.CommandLineException; import org.codehaus.plexus.util.cli.CommandLineUtils; import org.codehaus.plexus.util.cli.Commandline; import org.codehaus.plexus.util.cli.StreamConsumer; import org.codehaus.plexus.util.xml.pull.XmlPullParserException; /** * Provides common code for mojos invoking sub builds. * * @author Stephen Connolly * @since 15-Aug-2009 09:09:29 */ public abstract class AbstractInvokerMojo extends AbstractMojo { private static final int SELECTOR_MAVENVERSION = 1; private static final int SELECTOR_JREVERSION = 2; private static final int SELECTOR_OSFAMILY = 4; /** * Flag used to suppress certain invocations. This is useful in tailoring the build using profiles. * * @since 1.1 */ @Parameter(property = "invoker.skip", defaultValue = "false") private boolean skipInvocation; /** * Flag used to suppress the summary output notifying of successes and failures. If set to <code>true</code>, the * only indication of the build's success or failure will be the effect it has on the main build (if it fails, the * main build should fail as well). If {@link #streamLogs} is enabled, the sub-build summary will also provide an * indication. */ @Parameter(defaultValue = "false") protected boolean suppressSummaries; /** * Flag used to determine whether the build logs should be output to the normal mojo log. */ @Parameter(property = "invoker.streamLogs", defaultValue = "false") private boolean streamLogs; /** * The local repository for caching artifacts. It is strongly recommended to specify a path to an isolated * repository like <code>${project.build.directory}/it-repo</code>. Otherwise, your ordinary local repository will * be used, potentially soiling it with broken artifacts. */ @Parameter(property = "invoker.localRepositoryPath", defaultValue = "${settings.localRepository}") private File localRepositoryPath; /** * Directory to search for integration tests. */ @Parameter(property = "invoker.projectsDirectory", defaultValue = "${basedir}/src/it/") private File projectsDirectory; /** * Base directory where all build reports are written to. * Every execution of an integration test will produce an XML file which contains the information * about success or failure of that particular build job. The format of the resulting XML * file is documented in the given <a href="./build-job.html">build-job</a> reference. * * @since 1.4 */ @Parameter(property = "invoker.reportsDirectory", defaultValue = "${project.build.directory}/invoker-reports") private File reportsDirectory; /** * A flag to disable the generation of build reports. * * @since 1.4 */ @Parameter(property = "invoker.disableReports", defaultValue = "false") private boolean disableReports; /** * Directory to which projects should be cloned prior to execution. If not specified, each integration test will be * run in the directory in which the corresponding IT POM was found. In this case, you most likely want to configure * your SCM to ignore <code>target</code> and <code>build.log</code> in the test's base directory. * * @since 1.1 */ @Parameter private File cloneProjectsTo; /** * Some files are normally excluded when copying the IT projects from the directory specified by the parameter * projectsDirectory to the directory given by cloneProjectsTo (e.g. <code>.svn</code>, <code>CVS</code>, * <code>*~</code>, etc). Setting this parameter to <code>true</code> will cause all files to be copied to the * cloneProjectsTo directory. * * @since 1.2 */ @Parameter(defaultValue = "false") private boolean cloneAllFiles; /** * Ensure the {@link #cloneProjectsTo} directory is not polluted with files from earlier invoker runs. * * @since 1.6 */ @Parameter(defaultValue = "false") private boolean cloneClean; /** * A single POM to build, skipping any scanning parameters and behavior. */ @Parameter(property = "invoker.pom") private File pom; /** * Include patterns for searching the integration test directory for projects. This parameter is meant to be set * from the POM. If this parameter is not set, the plugin will search for all <code>pom.xml</code> files one * directory below {@link #projectsDirectory} (i.e. <code>*/pom.xml</code>).<br> * <br> * Starting with version 1.3, mere directories can also be matched by these patterns. For example, the include * pattern <code>*</code> will run Maven builds on all immediate sub directories of {@link #projectsDirectory}, * regardless if they contain a <code>pom.xml</code>. This allows to perform builds that need/should not depend on * the existence of a POM. */ @Parameter private List<String> pomIncludes = Collections.singletonList("*/pom.xml"); /** * Exclude patterns for searching the integration test directory. This parameter is meant to be set from the POM. By * default, no POM files are excluded. For the convenience of using an include pattern like <code>*</code>, the * custom settings file specified by the parameter {@link #settingsFile} will always be excluded automatically. */ @Parameter private List<String> pomExcludes = Collections.emptyList(); /** * Include patterns for searching the projects directory for projects that need to be run before the other projects. * This parameter allows to declare projects that perform setup tasks like installing utility artifacts into the * local repository. Projects matched by these patterns are implicitly excluded from the scan for ordinary projects. * Also, the exclusions defined by the parameter {@link #pomExcludes} apply to the setup projects, too. Default * value is: <code>setup*/pom.xml</code>. * * @since 1.3 */ @Parameter private List<String> setupIncludes = Collections.singletonList("setup*/pom.xml"); /** * The list of goals to execute on each project. Default value is: <code>package</code>. */ @Parameter private List<String> goals = Collections.singletonList("package"); /** * The name of the project-specific file that contains the enumeration of goals to execute for that test. * * @deprecated As of version 1.2, the key <code>invoker.goals</code> from the properties file specified by the * parameter {@link #invokerPropertiesFile} should be used instead. */ @Parameter(property = "invoker.goalsFile", defaultValue = "goals.txt") private String goalsFile; /** */ @Component private Invoker invoker; /** * Relative path of a selector script to run prior in order to decide if the build should be executed. This script * may be written with either BeanShell or Groovy. If the file extension is omitted (e.g. <code>selector</code>), * the plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>. * If this script exists for a particular project but returns any non-null value different from <code>true</code>, * the corresponding build is flagged as skipped. In this case, none of the pre-build hook script, * Maven nor the post-build hook script will be invoked. If this script throws an exception, the corresponding * build is flagged as in error, and none of the pre-build hook script, Maven not the post-build hook script will * be invoked. * * @since 1.5 */ @Parameter(property = "invoker.selectorScript", defaultValue = "selector") private String selectorScript; /** * Relative path of a pre-build hook script to run prior to executing the build. This script may be written with * either BeanShell or Groovy (since 1.3). If the file extension is omitted (e.g. <code>prebuild</code>), the plugin * searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>. If this * script exists for a particular project but returns any non-null value different from <code>true</code> or throws * an exception, the corresponding build is flagged as a failure. In this case, neither Maven nor the post-build * hook script will be invoked. */ @Parameter(property = "invoker.preBuildHookScript", defaultValue = "prebuild") private String preBuildHookScript; /** * Relative path of a cleanup/verification hook script to run after executing the build. This script may be written * with either BeanShell or Groovy (since 1.3). If the file extension is omitted (e.g. <code>verify</code>), the * plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>. * If this script exists for a particular project but returns any non-null value different from <code>true</code> or * throws an exception, the corresponding build is flagged as a failure. */ @Parameter(property = "invoker.postBuildHookScript", defaultValue = "postbuild") private String postBuildHookScript; /** * Location of a properties file that defines CLI properties for the test. */ @Parameter(property = "invoker.testPropertiesFile", defaultValue = "test.properties") private String testPropertiesFile; /** * Common set of test properties to pass in on each IT's command line, via -D parameters. * * @deprecated As of version 1.1, use the {@link #properties} parameter instead. */ @Parameter private Properties testProperties; /** * Common set of properties to pass in on each project's command line, via -D parameters. * * @since 1.1 */ @Parameter private Map<String, String> properties; /** * Whether to show errors in the build output. */ @Parameter(property = "invoker.showErrors", defaultValue = "false") private boolean showErrors; /** * Whether to show debug statements in the build output. */ @Parameter(property = "invoker.debug", defaultValue = "false") private boolean debug; /** * Suppress logging to the <code>build.log</code> file. */ @Parameter(property = "invoker.noLog", defaultValue = "false") private boolean noLog; /** * List of profile identifiers to explicitly trigger in the build. * * @since 1.1 */ @Parameter private List<String> profiles; /** * List of properties which will be used to interpolate goal files. * * @since 1.1 * @deprecated As of version 1.3, the parameter {@link #filterProperties} should be used instead. */ @Parameter private Properties interpolationsProperties; /** * A list of additional properties which will be used to filter tokens in POMs and goal files. * * @since 1.3 */ @Parameter private Map<String, String> filterProperties; /** * The Maven Project Object * * @since 1.1 */ @Component private MavenProject project; @Component private MojoExecution mojoExecution; /** * A comma separated list of projectname patterns to run. Specify this parameter to run individual tests by file name, * overriding the {@link #setupIncludes}, {@link #pomIncludes} and {@link #pomExcludes} parameters. Each pattern you * specify here will be used to create an include/exclude pattern formatted like * <code>${projectsDirectory}/<i>pattern</i></code>. To exclude a test, prefix the pattern with a '<code>!</code>'. * So you can just type * <nobr><code>-Dinvoker.test=SimpleTest,Comp*Test,!Compare*</code></nobr> to run builds in * <code>${projectsDirectory}/SimpleTest</code> and * <code>${projectsDirectory}/ComplexTest</code>, but not <code>${projectsDirectory}/CompareTest</code> * * @since 1.1 (exclusion since 1.8) */ @Parameter(property = "invoker.test") private String invokerTest; /** * The name of the project-specific file that contains the enumeration of profiles to use for that test. <b>If the * file exists and is empty no profiles will be used even if the parameter {@link #profiles} is set.</b> * * @since 1.1 * @deprecated As of version 1.2, the key <code>invoker.profiles</code> from the properties file specified by the * parameter {@link #invokerPropertiesFile} should be used instead. */ @Parameter(property = "invoker.profilesFile", defaultValue = "profiles.txt") private String profilesFile; /** * Path to an alternate <code>settings.xml</code> to use for Maven invocation with all ITs. Note that the * <code><localRepository></code> element of this settings file is always ignored, i.e. the path given by the * parameter {@link #localRepositoryPath} is dominant. * * @since 1.2 */ @Parameter(property = "invoker.settingsFile") private File settingsFile; /** * The <code>MAVEN_OPTS</code> environment variable to use when invoking Maven. This value can be overridden for * individual integration tests by using {@link #invokerPropertiesFile}. * * @since 1.2 */ @Parameter(property = "invoker.mavenOpts") private String mavenOpts; /** * The home directory of the Maven installation to use for the forked builds. Defaults to the current Maven * installation. * * @since 1.3 */ @Parameter(property = "invoker.mavenHome") private File mavenHome; /** * mavenExecutable can either be a file relative to <code>${maven.home}/bin/</code> or an absolute file. * * @since 1.8 * @see Invoker#setMavenExecutable(File) */ @Parameter(property = "invoker.mavenExecutable") private String mavenExecutable; /** * The <code>JAVA_HOME</code> environment variable to use for forked Maven invocations. Defaults to the current Java * home directory. * * @since 1.3 */ @Parameter(property = "invoker.javaHome") private File javaHome; /** * The file encoding for the pre-/post-build scripts and the list files for goals and profiles. * * @since 1.2 */ @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}") private String encoding; /** * The current user system settings for use in Maven. * * @since 1.2 */ @Component private Settings settings; /** * A flag whether the test class path of the project under test should be included in the class path of the * pre-/post-build scripts. If set to <code>false</code>, the class path of script interpreter consists only of * the <a href="dependencies.html">runtime dependencies</a> of the Maven Invoker Plugin. If set the * <code>true</code>, the project's test class path will be prepended to the interpreter class path. Among * others, this feature allows the scripts to access utility classes from the test sources of your project. * * @since 1.2 */ @Parameter(property = "invoker.addTestClassPath", defaultValue = "false") private boolean addTestClassPath; /** * The test class path of the project under test. */ @Parameter(defaultValue = "${project.testClasspathElements}", readonly = true) private List<String> testClassPath; /** * The name of an optional project-specific file that contains properties used to specify settings for an individual * Maven invocation. Any property present in the file will override the corresponding setting from the plugin * configuration. The values of the properties are filtered and may use expressions like * <code>${project.version}</code> to reference project properties or values from the parameter * {@link #filterProperties}. The snippet below describes the supported properties: * <p/> * <pre> * # A comma or space separated list of goals/phases to execute, may * # specify an empty list to execute the default goal of the IT project * invoker.goals = clean install * * # Optionally, a list of goals to run during further invocations of Maven * invoker.goals.2 = ${project.groupId}:${project.artifactId}:${project.version}:run * * # A comma or space separated list of profiles to activate * invoker.profiles = its,jdk15 * * # The path to an alternative POM or base directory to invoke Maven on, defaults to the * # project that was originally specified in the plugin configuration * # Since plugin version 1.4 * invoker.project = sub-module * * # The value for the environment variable MAVEN_OPTS * invoker.mavenOpts = -Dfile.encoding=UTF-16 -Xms32m -Xmx256m * * # Possible values are "fail-fast" (default), "fail-at-end" and "fail-never" * invoker.failureBehavior = fail-never * * # The expected result of the build, possible values are "success" (default) and "failure" * invoker.buildResult = failure * * # A boolean value controlling the aggregator mode of Maven, defaults to "false" * invoker.nonRecursive = true * * # A boolean value controlling the network behavior of Maven, defaults to "false" * # Since plugin version 1.4 * invoker.offline = true * * # The path to the properties file from which to load system properties, defaults to the * # filename given by the plugin parameter testPropertiesFile * # Since plugin version 1.4 * invoker.systemPropertiesFile = test.properties * * # An optional human friendly name for this build job to be included in the build reports. * # Since plugin version 1.4 * invoker.name = Test Build 01 * * # An optional description for this build job to be included in the build reports. * # Since plugin version 1.4 * invoker.description = Checks the support for build reports. * * # A comma separated list of JRE versions on which this build job should be run. * # Since plugin version 1.4 * invoker.java.version = 1.4+, !1.4.1, 1.7- * * # A comma separated list of OS families on which this build job should be run. * # Since plugin version 1.4 * invoker.os.family = !windows, unix, mac * * # A comma separated list of Maven versions on which this build should be run. * # Since plugin version 1.5 * invoker.maven.version = 2.0.10+, !2.1.0, !2.2.0 * * # A boolean value controlling the debug logging level of Maven, , defaults to "false" * # Since plugin version 1.8 * invoker.debug = true * </pre> * * @since 1.2 */ @Parameter(property = "invoker.invokerPropertiesFile", defaultValue = "invoker.properties") private String invokerPropertiesFile; /** * flag to enable show mvn version used for running its (cli option : -V,--show-version ) * * @since 1.4 */ @Parameter(property = "invoker.showVersion", defaultValue = "false") private boolean showVersion; /** * number of threads for running tests in parallel. * This will be the number of maven forked process in parallel. * * @since 1.6 */ @Parameter(property = "invoker.parallelThreads", defaultValue = "1") private int parallelThreads; /** * @since 1.6 */ @Parameter(property = "plugin.artifacts", required = true, readonly = true) private List<Artifact> pluginArtifacts; /** * If enable and if you have a settings file configured for the execution, it will be merged with your user settings. * * @since 1.6 */ @Parameter(property = "invoker.mergeUserSettings", defaultValue = "false") private boolean mergeUserSettings; /** * Additional environment variables to set on the command line. * @since 1.8 */ @Parameter private Map<String, String> environmentVariables; /** * Additional variables for use in the hook scripts. * @since 1.9 */ @Parameter private Map<String, String> scriptVariables; /** * The scripter runner that is responsible to execute hook scripts. */ private ScriptRunner scriptRunner; /** * A string used to prefix the file name of the filtered POMs in case the POMs couldn't be filtered in-place (i.e. * the projects were not cloned to a temporary directory), can be <code>null</code>. This will be set to * <code>null</code> if the POMs have already been filtered during cloning. */ private String filteredPomPrefix = "interpolated-"; /** * The format for elapsed build time. */ private final DecimalFormat secFormat = new DecimalFormat("(0.0 s)", new DecimalFormatSymbols(Locale.ENGLISH)); /** * The version of Maven which is used to run the builds */ private String actualMavenVersion; /** * The version of the JRE which is used to run the builds */ private String actualJreVersion; private void setActualJreVersion(String actualJreVersion) { this.actualJreVersion = actualJreVersion; } /** * Invokes Maven on the configured test projects. * * @throws org.apache.maven.plugin.MojoExecutionException * If the goal encountered severe errors. * @throws org.apache.maven.plugin.MojoFailureException * If any of the Maven builds failed. */ public void execute() throws MojoExecutionException, MojoFailureException { if (skipInvocation) { getLog().info("Skipping invocation per configuration." + " If this is incorrect, ensure the skipInvocation parameter is not set to true."); return; } // done it here to prevent issues with concurrent access in case of parallel run if (!disableReports && !reportsDirectory.exists()) { reportsDirectory.mkdirs(); } BuildJob[] buildJobs; if (pom != null) { try { projectsDirectory = pom.getCanonicalFile().getParentFile(); } catch (IOException e) { throw new MojoExecutionException( "Failed to discover projectsDirectory from pom File parameter. Reason: " + e.getMessage(), e); } buildJobs = new BuildJob[] { new BuildJob(pom.getName(), BuildJob.Type.NORMAL) }; } else { try { buildJobs = getBuildJobs(); } catch (final IOException e) { throw new MojoExecutionException( "Error retrieving POM list from includes, excludes, and projects directory. Reason: " + e.getMessage(), e); } } if ((buildJobs == null) || (buildJobs.length < 1)) { doFailIfNoProjects(); getLog().info("No projects were selected for execution."); return; } if (StringUtils.isEmpty(encoding)) { getLog().warn("File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING + ", i.e. build is platform dependent!"); } final List<String> scriptClassPath; if (addTestClassPath) { scriptClassPath = new ArrayList<String>(testClassPath); for (Artifact pluginArtifact : pluginArtifacts) { scriptClassPath.remove(pluginArtifact.getFile().getAbsolutePath()); } } else { scriptClassPath = null; } scriptRunner = new ScriptRunner(getLog()); scriptRunner.setScriptEncoding(encoding); scriptRunner.setGlobalVariable("localRepositoryPath", localRepositoryPath); if (scriptVariables != null) { for (Entry<String, String> entry : scriptVariables.entrySet()) { scriptRunner.setGlobalVariable(entry.getKey(), entry.getValue()); } } scriptRunner.setClassPath(scriptClassPath); Collection<String> collectedProjects = new LinkedHashSet<String>(); for (BuildJob buildJob : buildJobs) { collectProjects(projectsDirectory, buildJob.getProject(), collectedProjects, true); } File projectsDir = projectsDirectory; if (cloneProjectsTo != null) { cloneProjects(collectedProjects); projectsDir = cloneProjectsTo; } else { getLog().warn("Filtering of parent/child POMs is not supported without cloning the projects"); } runBuilds(projectsDir, buildJobs); processResults(new InvokerSession(buildJobs)); } protected void doFailIfNoProjects() throws MojoFailureException { // should only be used during run and verify } /** * Processes the results of invoking the build jobs. * * @param invokerSession The session with the build jobs, must not be <code>null</code>. * @throws MojoFailureException If the mojo had failed as a result of invoking the build jobs. * @since 1.4 */ abstract void processResults(InvokerSession invokerSession) throws MojoFailureException; /** * Creates a new reader for the specified file, using the plugin's {@link #encoding} parameter. * * @param file The file to create a reader for, must not be <code>null</code>. * @return The reader for the file, never <code>null</code>. * @throws java.io.IOException If the specified file was not found or the configured encoding is not supported. */ private Reader newReader(File file) throws IOException { if (StringUtils.isNotEmpty(encoding)) { return ReaderFactory.newReader(file, encoding); } else { return ReaderFactory.newPlatformReader(file); } } /** * Collects all projects locally reachable from the specified project. The method will as such try to read the POM * and recursively follow its parent/module elements. * * @param projectsDir The base directory of all projects, must not be <code>null</code>. * @param projectPath The relative path of the current project, can denote either the POM or its base directory, * must not be <code>null</code>. * @param projectPaths The set of already collected projects to add new projects to, must not be <code>null</code>. * This set will hold the relative paths to either a POM file or a project base directory. * @param included A flag indicating whether the specified project has been explicitly included via the parameter * {@link #pomIncludes}. Such projects will always be added to the result set even if there is no * corresponding POM. * @throws org.apache.maven.plugin.MojoExecutionException * If the project tree could not be traversed. */ private void collectProjects(File projectsDir, String projectPath, Collection<String> projectPaths, boolean included) throws MojoExecutionException { projectPath = projectPath.replace('\\', '/'); File pomFile = new File(projectsDir, projectPath); if (pomFile.isDirectory()) { pomFile = new File(pomFile, "pom.xml"); if (!pomFile.exists()) { if (included) { projectPaths.add(projectPath); } return; } if (!projectPath.endsWith("/")) { projectPath += '/'; } projectPath += "pom.xml"; } else if (!pomFile.isFile()) { return; } if (!projectPaths.add(projectPath)) { return; } getLog().debug("Collecting parent/child projects of " + projectPath); Model model = PomUtils.loadPom(pomFile); try { String projectsRoot = projectsDir.getCanonicalPath(); String projectDir = pomFile.getParent(); String parentPath = "../pom.xml"; if (model.getParent() != null && StringUtils.isNotEmpty(model.getParent().getRelativePath())) { parentPath = model.getParent().getRelativePath(); } String parent = relativizePath(new File(projectDir, parentPath), projectsRoot); if (parent != null) { collectProjects(projectsDir, parent, projectPaths, false); } Collection<String> modulePaths = new LinkedHashSet<String>(); modulePaths.addAll(model.getModules()); for (Profile profile : model.getProfiles()) { modulePaths.addAll(profile.getModules()); } for (String modulePath : modulePaths) { String module = relativizePath(new File(projectDir, modulePath), projectsRoot); if (module != null) { collectProjects(projectsDir, module, projectPaths, false); } } } catch (IOException e) { throw new MojoExecutionException("Failed to analyze POM: " + pomFile, e); } } /** * Copies the specified projects to the directory given by {@link #cloneProjectsTo}. A project may either be denoted * by a path to a POM file or merely by a path to a base directory. During cloning, the POM files will be filtered. * * @param projectPaths The paths to the projects to clone, relative to the projects directory, must not be * <code>null</code> nor contain <code>null</code> elements. * @throws org.apache.maven.plugin.MojoExecutionException * If the the projects could not be copied/filtered. */ private void cloneProjects(Collection<String> projectPaths) throws MojoExecutionException { if (!cloneProjectsTo.mkdirs() && cloneClean) { try { FileUtils.cleanDirectory(cloneProjectsTo); } catch (IOException e) { throw new MojoExecutionException( "Could not clean the cloneProjectsTo directory. Reason: " + e.getMessage(), e); } } // determine project directories to clone Collection<String> dirs = new LinkedHashSet<String>(); for (String projectPath : projectPaths) { if (!new File(projectsDirectory, projectPath).isDirectory()) { projectPath = getParentPath(projectPath); } dirs.add(projectPath); } boolean filter; // clone project directories try { filter = !cloneProjectsTo.getCanonicalFile().equals(projectsDirectory.getCanonicalFile()); List<String> clonedSubpaths = new ArrayList<String>(); for (String subpath : dirs) { // skip this project if its parent directory is also scheduled for cloning if (!".".equals(subpath) && dirs.contains(getParentPath(subpath))) { continue; } // avoid copying subdirs that are already cloned. if (!alreadyCloned(subpath, clonedSubpaths)) { // avoid creating new files that point to dir/. if (".".equals(subpath)) { String cloneSubdir = relativizePath(cloneProjectsTo, projectsDirectory.getCanonicalPath()); // avoid infinite recursion if the cloneTo path is a subdirectory. if (cloneSubdir != null) { File temp = File.createTempFile("pre-invocation-clone.", ""); temp.delete(); temp.mkdirs(); copyDirectoryStructure(projectsDirectory, temp); FileUtils.deleteDirectory(new File(temp, cloneSubdir)); copyDirectoryStructure(temp, cloneProjectsTo); } else { copyDirectoryStructure(projectsDirectory, cloneProjectsTo); } } else { File srcDir = new File(projectsDirectory, subpath); File dstDir = new File(cloneProjectsTo, subpath); copyDirectoryStructure(srcDir, dstDir); } clonedSubpaths.add(subpath); } } } catch (IOException e) { throw new MojoExecutionException("Failed to clone projects from: " + projectsDirectory + " to: " + cloneProjectsTo + ". Reason: " + e.getMessage(), e); } // filter cloned POMs if (filter) { for (String projectPath : projectPaths) { File pomFile = new File(cloneProjectsTo, projectPath); if (pomFile.isFile()) { buildInterpolatedFile(pomFile, pomFile); } } filteredPomPrefix = null; } } /** * Gets the parent path of the specified relative path. * * @param path The relative path whose parent should be retrieved, must not be <code>null</code>. * @return The parent path or "." if the specified path has no parent, never <code>null</code>. */ private String getParentPath(String path) { int lastSep = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); return (lastSep < 0) ? "." : path.substring(0, lastSep); } /** * Copied a directory structure with deafault exclusions (.svn, CVS, etc) * * @param sourceDir The source directory to copy, must not be <code>null</code>. * @param destDir The target directory to copy to, must not be <code>null</code>. * @throws java.io.IOException If the directory structure could not be copied. */ private void copyDirectoryStructure(File sourceDir, File destDir) throws IOException { DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir(sourceDir); if (!cloneAllFiles) { scanner.addDefaultExcludes(); } scanner.scan(); /* * NOTE: Make sure the destination directory is always there (even if empty) to support POM-less ITs. */ destDir.mkdirs(); for (String includedDir : scanner.getIncludedDirectories()) { File clonedDir = new File(destDir, includedDir); clonedDir.mkdirs(); } for (String includedFile : scanner.getIncludedFiles()) { File sourceFile = new File(sourceDir, includedFile); File destFile = new File(destDir, includedFile); FileUtils.copyFile(sourceFile, destFile); } } /** * Determines whether the specified sub path has already been cloned, i.e. whether one of its ancestor directories * was already cloned. * * @param subpath The sub path to check, must not be <code>null</code>. * @param clonedSubpaths The list of already cloned paths, must not be <code>null</code> nor contain * <code>null</code> elements. * @return <code>true</code> if the specified path has already been cloned, <code>false</code> otherwise. */ static boolean alreadyCloned(String subpath, List<String> clonedSubpaths) { for (String path : clonedSubpaths) { if (".".equals(path) || subpath.equals(path) || subpath.startsWith(path + File.separator)) { return true; } } return false; } /** * Runs the specified build jobs. * * @param projectsDir The base directory of all projects, must not be <code>null</code>. * @param buildJobs The build jobs to run must not be <code>null</code> nor contain <code>null</code> elements. * @throws org.apache.maven.plugin.MojoExecutionException * If any build could not be launched. */ private void runBuilds(final File projectsDir, BuildJob[] buildJobs) throws MojoExecutionException { if (!localRepositoryPath.exists()) { localRepositoryPath.mkdirs(); } //----------------------------------------------- // interpolate settings file //----------------------------------------------- File interpolatedSettingsFile = null; if (settingsFile != null) { if (cloneProjectsTo != null) { interpolatedSettingsFile = new File(cloneProjectsTo, "interpolated-" + settingsFile.getName()); } else { interpolatedSettingsFile = new File(settingsFile.getParentFile(), "interpolated-" + settingsFile.getName()); } buildInterpolatedFile(settingsFile, interpolatedSettingsFile); } //----------------------------------------------- // merge settings file //----------------------------------------------- SettingsXpp3Writer settingsWriter = new SettingsXpp3Writer(); File mergedSettingsFile; Settings mergedSettings = this.settings; if (mergeUserSettings) { if (interpolatedSettingsFile != null) { // Have to merge the specified settings file (dominant) and the one of the invoking Maven process Reader reader = null; try { reader = new XmlStreamReader(interpolatedSettingsFile); SettingsXpp3Reader settingsReader = new SettingsXpp3Reader(); Settings dominantSettings = settingsReader.read(reader); // MINVOKER-137: NPE on dominantSettings.getRuntimeInfo() // DefaultMavenSettingsBuilder does the same trick if (dominantSettings.getRuntimeInfo() == null) { RuntimeInfo rtInfo = new RuntimeInfo(dominantSettings); rtInfo.setFile(interpolatedSettingsFile); dominantSettings.setRuntimeInfo(rtInfo); } Settings recessiveSettings = cloneSettings(); SettingsUtils.merge(dominantSettings, recessiveSettings, TrackableBase.USER_LEVEL); mergedSettings = dominantSettings; getLog().debug("Merged specified settings file with settings of invoking process"); } catch (XmlPullParserException e) { throw new MojoExecutionException("Could not read specified settings file", e); } catch (IOException e) { throw new MojoExecutionException("Could not read specified settings file", e); } finally { IOUtil.close(reader); } } } if (this.settingsFile != null && !mergeUserSettings) { mergedSettingsFile = interpolatedSettingsFile; } else { try { mergedSettingsFile = File.createTempFile("invoker-settings", ".xml"); FileWriter fileWriter = null; try { fileWriter = new FileWriter(mergedSettingsFile); settingsWriter.write(fileWriter, mergedSettings); } finally { IOUtil.close(fileWriter); } if (getLog().isDebugEnabled()) { getLog().debug("Created temporary file for invoker settings.xml: " + mergedSettingsFile.getAbsolutePath()); } } catch (IOException e) { throw new MojoExecutionException("Could not create temporary file for invoker settings.xml", e); } } final File finalSettingsFile = mergedSettingsFile; if (mavenHome != null) { actualMavenVersion = SelectorUtils.getMavenVersion(mavenHome); } else { actualMavenVersion = SelectorUtils.getMavenVersion(); } scriptRunner.setGlobalVariable("mavenVersion", actualMavenVersion); if (javaHome != null) { resolveExternalJreVersion(); } else { actualJreVersion = SelectorUtils.getJreVersion(); } try { if (isParallelRun()) { getLog().info("use parallelThreads " + parallelThreads); ExecutorService executorService = Executors.newFixedThreadPool(parallelThreads); for (final BuildJob job : buildJobs) { executorService.execute(new Runnable() { public void run() { try { runBuild(projectsDir, job, finalSettingsFile); } catch (MojoExecutionException e) { throw new RuntimeException(e.getMessage(), e); } } }); } try { executorService.shutdown(); // TODO add a configurable time out executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { throw new MojoExecutionException(e.getMessage(), e); } } else { for (BuildJob job : buildJobs) { runBuild(projectsDir, job, finalSettingsFile); } } } finally { if (interpolatedSettingsFile != null && cloneProjectsTo == null) { interpolatedSettingsFile.delete(); } if (mergedSettingsFile != null && mergedSettingsFile.exists()) { mergedSettingsFile.delete(); } } } private Settings cloneSettings() { Settings recessiveSettings = SettingsUtils.copySettings(this.settings); // MINVOKER-133: reset sourceLevelSet resetSourceLevelSet(recessiveSettings); for (org.apache.maven.settings.Mirror mirror : recessiveSettings.getMirrors()) { resetSourceLevelSet(mirror); } for (org.apache.maven.settings.Server server : recessiveSettings.getServers()) { resetSourceLevelSet(server); } for (org.apache.maven.settings.Proxy proxy : recessiveSettings.getProxies()) { resetSourceLevelSet(proxy); } for (org.apache.maven.settings.Profile profile : recessiveSettings.getProfiles()) { resetSourceLevelSet(profile); } return recessiveSettings; } private void resetSourceLevelSet(org.apache.maven.settings.TrackableBase trackable) { try { ReflectionUtils.setVariableValueInObject(trackable, "sourceLevelSet", Boolean.FALSE); getLog().debug("sourceLevelSet: " + ReflectionUtils.getValueIncludingSuperclasses("sourceLevelSet", trackable)); } catch (IllegalAccessException e) { //noop } } private void resolveExternalJreVersion() { Artifact pluginArtifact = mojoExecution.getMojoDescriptor().getPluginDescriptor().getPluginArtifact(); pluginArtifact.getFile(); Commandline commandLine = new Commandline(); commandLine.setExecutable(new File(javaHome, "bin/java").getAbsolutePath()); commandLine.createArg().setValue("-cp"); commandLine.createArg().setFile(pluginArtifact.getFile()); commandLine.createArg().setValue(SystemPropertyPrinter.class.getName()); commandLine.createArg().setValue("java.version"); StreamConsumer consumer = new StreamConsumer() { public void consumeLine(String line) { setActualJreVersion(line); } }; try { CommandLineUtils.executeCommandLine(commandLine, consumer, null); } catch (CommandLineException e) { getLog().warn(e.getMessage()); } } /** * Runs the specified project. * * @param projectsDir The base directory of all projects, must not be <code>null</code>. * @param buildJob The build job to run, must not be <code>null</code>. * @param settingsFile The (already interpolated) user settings file for the build, may be <code>null</code> to use * the current user settings. * @throws org.apache.maven.plugin.MojoExecutionException * If the project could not be launched. */ private void runBuild(File projectsDir, BuildJob buildJob, File settingsFile) throws MojoExecutionException { File pomFile = new File(projectsDir, buildJob.getProject()); File basedir; if (pomFile.isDirectory()) { basedir = pomFile; pomFile = new File(basedir, "pom.xml"); if (!pomFile.exists()) { pomFile = null; } else { buildJob.setProject(buildJob.getProject() + File.separator + "pom.xml"); } } else { basedir = pomFile.getParentFile(); } getLog().info("Building: " + buildJob.getProject()); File interpolatedPomFile = null; if (pomFile != null) { if (filteredPomPrefix != null) { interpolatedPomFile = new File(basedir, filteredPomPrefix + pomFile.getName()); buildInterpolatedFile(pomFile, interpolatedPomFile); } else { interpolatedPomFile = pomFile; } } InvokerProperties invokerProperties = getInvokerProperties(basedir); // let's set what details we can buildJob.setName(invokerProperties.getJobName()); buildJob.setDescription(invokerProperties.getJobDescription()); try { int selection = getSelection(invokerProperties); if (selection == 0) { long milliseconds = System.currentTimeMillis(); boolean executed; try { executed = runBuild(basedir, interpolatedPomFile, settingsFile, invokerProperties); } finally { milliseconds = System.currentTimeMillis() - milliseconds; buildJob.setTime(milliseconds / 1000.0); } if (executed) { buildJob.setResult(BuildJob.Result.SUCCESS); if (!suppressSummaries) { getLog().info("..SUCCESS " + formatTime(buildJob.getTime())); } } else { buildJob.setResult(BuildJob.Result.SKIPPED); if (!suppressSummaries) { getLog().info("..SKIPPED " + formatTime(buildJob.getTime())); } } } else { buildJob.setResult(BuildJob.Result.SKIPPED); StringBuilder message = new StringBuilder(); if ((selection & SELECTOR_MAVENVERSION) != 0) { message.append("Maven version"); } if ((selection & SELECTOR_JREVERSION) != 0) { if (message.length() > 0) { message.append(", "); } message.append("JRE version"); } if ((selection & SELECTOR_OSFAMILY) != 0) { if (message.length() > 0) { message.append(", "); } message.append("OS"); } if (!suppressSummaries) { getLog().info("..SKIPPED due to " + message.toString()); } // Abuse failureMessage, the field in the report which should contain the reason for skipping // Consider skipCode + I18N buildJob.setFailureMessage("Skipped due to " + message.toString()); } } catch (RunErrorException e) { buildJob.setResult(BuildJob.Result.ERROR); buildJob.setFailureMessage(e.getMessage()); if (!suppressSummaries) { getLog().info("..ERROR " + formatTime(buildJob.getTime())); getLog().info(" " + e.getMessage()); } } catch (RunFailureException e) { buildJob.setResult(e.getType()); buildJob.setFailureMessage(e.getMessage()); if (!suppressSummaries) { getLog().info("..FAILED " + formatTime(buildJob.getTime())); getLog().info(" " + e.getMessage()); } } finally { if (interpolatedPomFile != null && StringUtils.isNotEmpty(filteredPomPrefix)) { interpolatedPomFile.delete(); } writeBuildReport(buildJob); } } /** * Determines whether selector conditions of the specified invoker properties match the current environment. * * @param invokerProperties The invoker properties to check, must not be <code>null</code>. * @return <code>0</code> if the job corresponding to the properties should be run, * otherwise a bitwise value representing the reason why it should be skipped. */ private int getSelection(InvokerProperties invokerProperties) { int selection = 0; if (!SelectorUtils.isMavenVersion(invokerProperties.getMavenVersion(), actualMavenVersion)) { selection |= SELECTOR_MAVENVERSION; } if (!SelectorUtils.isJreVersion(invokerProperties.getJreVersion(), actualJreVersion)) { selection |= SELECTOR_JREVERSION; } if (!SelectorUtils.isOsFamily(invokerProperties.getOsFamily())) { selection |= SELECTOR_OSFAMILY; } return selection; } /** * Writes the XML report for the specified build job unless report generation has been disabled. * * @param buildJob The build job whose report should be written, must not be <code>null</code>. * @throws org.apache.maven.plugin.MojoExecutionException * If the report could not be written. */ private void writeBuildReport(BuildJob buildJob) throws MojoExecutionException { if (disableReports) { return; } String safeFileName = buildJob.getProject().replace('/', '_').replace('\\', '_').replace(' ', '_'); if (safeFileName.endsWith("_pom.xml")) { safeFileName = safeFileName.substring(0, safeFileName.length() - "_pom.xml".length()); } File reportFile = new File(reportsDirectory, "BUILD-" + safeFileName + ".xml"); try { FileOutputStream fos = new FileOutputStream(reportFile); try { Writer osw = new OutputStreamWriter(fos, buildJob.getModelEncoding()); BuildJobXpp3Writer writer = new BuildJobXpp3Writer(); writer.write(osw, buildJob); osw.close(); } finally { fos.close(); } } catch (IOException e) { throw new MojoExecutionException("Failed to write build report " + reportFile, e); } } /** * Formats the specified build duration time. * * @param seconds The duration of the build. * @return The formatted time, never <code>null</code>. */ private String formatTime(double seconds) { return secFormat.format(seconds); } /** * Runs the specified project. * * @param basedir The base directory of the project, must not be <code>null</code>. * @param pomFile The (already interpolated) POM file, may be <code>null</code> for a POM-less Maven invocation. * @param settingsFile The (already interpolated) user settings file for the build, may be <code>null</code>. Will be * merged with the settings file of the invoking Maven process. * @param invokerProperties The properties to use. * @return <code>true</code> if the project was launched or <code>false</code> if the selector script indicated that * the project should be skipped. * @throws org.apache.maven.plugin.MojoExecutionException * If the project could not be launched. * @throws org.apache.maven.shared.scriptinterpreter.RunFailureException * If either a hook script or the build itself failed. */ private boolean runBuild(File basedir, File pomFile, File settingsFile, InvokerProperties invokerProperties) throws MojoExecutionException, RunFailureException { if (getLog().isDebugEnabled() && !invokerProperties.getProperties().isEmpty()) { Properties props = invokerProperties.getProperties(); getLog().debug("Using invoker properties:"); for (String key : new TreeSet<String>((Set) props.keySet())) { String value = props.getProperty(key); getLog().debug(" " + key + " = " + value); } } List<String> goals = getGoals(basedir); List<String> profiles = getProfiles(basedir); Map<String, Object> context = new LinkedHashMap<String, Object>(); FileLogger logger = setupLogger(basedir); try { try { scriptRunner.run("selector script", basedir, selectorScript, context, logger, BuildJob.Result.SKIPPED, false); } catch (RunErrorException e) { throw e; } catch (RunFailureException e) { return false; } scriptRunner.run("pre-build script", basedir, preBuildHookScript, context, logger, BuildJob.Result.FAILURE_PRE_HOOK, false); final InvocationRequest request = new DefaultInvocationRequest(); request.setLocalRepositoryDirectory(localRepositoryPath); request.setInteractive(false); request.setShowErrors(showErrors); request.setDebug(debug); request.setShowVersion(showVersion); if (logger != null) { request.setErrorHandler(logger); request.setOutputHandler(logger); } if (mavenHome != null) { invoker.setMavenHome(mavenHome); request.addShellEnvironment("M2_HOME", mavenHome.getAbsolutePath()); } if (mavenExecutable != null) { invoker.setMavenExecutable(new File(mavenExecutable)); } if (javaHome != null) { request.setJavaHome(javaHome); } if (environmentVariables != null) { for (Map.Entry<String, String> variable : environmentVariables.entrySet()) { request.addShellEnvironment(variable.getKey(), variable.getValue()); } } for (int invocationIndex = 1;; invocationIndex++) { if (invocationIndex > 1 && !invokerProperties.isInvocationDefined(invocationIndex)) { break; } request.setBaseDirectory(basedir); request.setPomFile(pomFile); request.setGoals(goals); request.setProfiles(profiles); request.setMavenOpts(mavenOpts); request.setOffline(false); request.setUserSettingsFile(settingsFile); Properties systemProperties = getSystemProperties(basedir, invokerProperties.getSystemPropertiesFile(invocationIndex)); request.setProperties(systemProperties); invokerProperties.configureInvocation(request, invocationIndex); if (getLog().isDebugEnabled()) { try { getLog().debug("Using MAVEN_OPTS: " + request.getMavenOpts()); getLog().debug("Executing: " + new MavenCommandLineBuilder().build(request)); } catch (CommandLineConfigurationException e) { getLog().debug("Failed to display command line: " + e.getMessage()); } } InvocationResult result; try { result = invoker.execute(request); } catch (final MavenInvocationException e) { getLog().debug("Error invoking Maven: " + e.getMessage(), e); throw new RunFailureException("Maven invocation failed. " + e.getMessage(), BuildJob.Result.FAILURE_BUILD); } verify(result, invocationIndex, invokerProperties, logger); } scriptRunner.run("post-build script", basedir, postBuildHookScript, context, logger, BuildJob.Result.FAILURE_POST_HOOK, true); } catch (IOException e) { throw new MojoExecutionException(e.getMessage(), e); } finally { if (logger != null) { logger.close(); } } return true; } /** * Initializes the build logger for the specified project. * * @param basedir The base directory of the project, must not be <code>null</code>. * @return The build logger or <code>null</code> if logging has been disabled. * @throws org.apache.maven.plugin.MojoExecutionException * If the log file could not be created. */ private FileLogger setupLogger(File basedir) throws MojoExecutionException { FileLogger logger = null; if (!noLog) { File outputLog = new File(basedir, "build.log"); try { if (streamLogs) { logger = new FileLogger(outputLog, getLog()); } else { logger = new FileLogger(outputLog); } getLog().debug("Build log initialized in: " + outputLog); } catch (IOException e) { throw new MojoExecutionException("Error initializing build logfile in: " + outputLog, e); } } return logger; } /** * Gets the system properties to use for the specified project. * * @param basedir The base directory of the project, must not be <code>null</code>. * @param filename The filename to the properties file to load, may be <code>null</code> to use the default path * given by {@link #testPropertiesFile}. * @return The system properties to use, may be empty but never <code>null</code>. * @throws org.apache.maven.plugin.MojoExecutionException * If the properties file exists but could not be read. */ private Properties getSystemProperties(final File basedir, final String filename) throws MojoExecutionException { Properties collectedTestProperties = new Properties(); if (testProperties != null) { collectedTestProperties.putAll(testProperties); } if (properties != null) { // MINVOKER-118: property can have empty value, which is not accepted by collectedTestProperties for (Map.Entry<String, String> entry : properties.entrySet()) { if (entry.getValue() != null) { collectedTestProperties.put(entry.getKey(), entry.getValue()); } } } File propertiesFile = null; if (filename != null) { propertiesFile = new File(basedir, filename); } else if (testPropertiesFile != null) { propertiesFile = new File(basedir, testPropertiesFile); } if (propertiesFile != null && propertiesFile.isFile()) { InputStream fin = null; try { fin = new FileInputStream(propertiesFile); Properties loadedProperties = new Properties(); loadedProperties.load(fin); collectedTestProperties.putAll(loadedProperties); } catch (IOException e) { throw new MojoExecutionException("Error reading system properties from " + propertiesFile); } finally { IOUtil.close(fin); } } return collectedTestProperties; } /** * Verifies the invocation result. * * @param result The invocation result to check, must not be <code>null</code>. * @param invocationIndex The index of the invocation for which to check the exit code, must not be negative. * @param invokerProperties The invoker properties used to check the exit code, must not be <code>null</code>. * @param logger The build logger, may be <code>null</code> if logging is disabled. * @throws org.apache.maven.shared.scriptinterpreter.RunFailureException * If the invocation result indicates a build failure. */ private void verify(InvocationResult result, int invocationIndex, InvokerProperties invokerProperties, FileLogger logger) throws RunFailureException { if (result.getExecutionException() != null) { throw new RunFailureException( "The Maven invocation failed. " + result.getExecutionException().getMessage(), BuildJob.Result.ERROR); } else if (!invokerProperties.isExpectedResult(result.getExitCode(), invocationIndex)) { StringBuilder buffer = new StringBuilder(256); buffer.append("The build exited with code ").append(result.getExitCode()).append(". "); if (logger != null) { buffer.append("See "); buffer.append(logger.getOutputFile().getAbsolutePath()); buffer.append(" for details."); } else { buffer.append("See console output for details."); } throw new RunFailureException(buffer.toString(), BuildJob.Result.FAILURE_BUILD); } } /** * Gets the goal list for the specified project. * * @param basedir The base directory of the project, must not be <code>null</code>. * @return The list of goals to run when building the project, may be empty but never <code>null</code>. * @throws org.apache.maven.plugin.MojoExecutionException * If the profile file could not be read. */ List<String> getGoals(final File basedir) throws MojoExecutionException { try { return getTokens(basedir, goalsFile, goals); } catch (IOException e) { throw new MojoExecutionException("error reading goals", e); } } /** * Gets the profile list for the specified project. * * @param basedir The base directory of the project, must not be <code>null</code>. * @return The list of profiles to activate when building the project, may be empty but never <code>null</code>. * @throws org.apache.maven.plugin.MojoExecutionException * If the profile file could not be read. */ List<String> getProfiles(File basedir) throws MojoExecutionException { try { return getTokens(basedir, profilesFile, profiles); } catch (IOException e) { throw new MojoExecutionException("error reading profiles", e); } } /** * Gets the build jobs that should be processed. Note that the order of the returned build jobs is significant. * * @return The build jobs to process, may be empty but never <code>null</code>. * @throws java.io.IOException If the projects directory could not be scanned. */ BuildJob[] getBuildJobs() throws IOException { BuildJob[] buildJobs; if ((pom != null) && pom.exists()) { buildJobs = new BuildJob[] { new BuildJob(pom.getAbsolutePath(), BuildJob.Type.NORMAL) }; } else if (invokerTest != null) { String[] testRegexes = StringUtils.split(invokerTest, ","); List<String> includes = new ArrayList<String>(testRegexes.length); List<String> excludes = new ArrayList<String>(); for (String regex : testRegexes) { // user just use -Dinvoker.test=MWAR191,MNG111 to use a directory thats the end is not pom.xml if (regex.startsWith("!")) { excludes.add(regex.substring(1)); } else { includes.add(regex); } } // it would be nice if we could figure out what types these are... but perhaps // not necessary for the -Dinvoker.test=xxx t buildJobs = scanProjectsDirectory(includes, excludes, BuildJob.Type.DIRECT); } else { List<String> excludes = (pomExcludes != null) ? new ArrayList<String>(pomExcludes) : new ArrayList<String>(); if (this.settingsFile != null) { String exclude = relativizePath(this.settingsFile, projectsDirectory.getCanonicalPath()); if (exclude != null) { excludes.add(exclude.replace('\\', '/')); getLog().debug("Automatically excluded " + exclude + " from project scanning"); } } BuildJob[] setupPoms = scanProjectsDirectory(setupIncludes, excludes, BuildJob.Type.SETUP); if (getLog().isDebugEnabled()) { getLog().debug("Setup projects: " + Arrays.asList(setupPoms)); } BuildJob[] normalPoms = scanProjectsDirectory(pomIncludes, excludes, BuildJob.Type.NORMAL); Map<String, BuildJob> uniquePoms = new LinkedHashMap<String, BuildJob>(); for (BuildJob setupPom : setupPoms) { uniquePoms.put(setupPom.getProject(), setupPom); } for (BuildJob normalPom : normalPoms) { if (!uniquePoms.containsKey(normalPom.getProject())) { uniquePoms.put(normalPom.getProject(), normalPom); } } buildJobs = uniquePoms.values().toArray(new BuildJob[uniquePoms.size()]); } relativizeProjectPaths(buildJobs); return buildJobs; } /** * Scans the projects directory for projects to build. Both (POM) files and mere directories will be matched by the * scanner patterns. If the patterns match a directory which contains a file named "pom.xml", the results will * include the path to this file rather than the directory path in order to avoid duplicate invocations of the same * project. * * @param includes The include patterns for the scanner, may be <code>null</code>. * @param excludes The exclude patterns for the scanner, may be <code>null</code> to exclude nothing. * @param type The type to assign to the resulting build jobs, must not be <code>null</code>. * @return The build jobs matching the patterns, never <code>null</code>. * @throws java.io.IOException If the project directory could not be scanned. */ private BuildJob[] scanProjectsDirectory(List<String> includes, List<String> excludes, String type) throws IOException { if (!projectsDirectory.isDirectory()) { return new BuildJob[0]; } DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir(projectsDirectory.getCanonicalFile()); scanner.setFollowSymlinks(false); if (includes != null) { scanner.setIncludes(includes.toArray(new String[includes.size()])); } if (excludes != null) { scanner.setExcludes(excludes.toArray(new String[excludes.size()])); } scanner.addDefaultExcludes(); scanner.scan(); Map<String, BuildJob> matches = new LinkedHashMap<String, BuildJob>(); for (String includedFile : scanner.getIncludedFiles()) { matches.put(includedFile, new BuildJob(includedFile, type)); } for (String includedDir : scanner.getIncludedDirectories()) { String includedFile = includedDir + File.separatorChar + "pom.xml"; if (new File(scanner.getBasedir(), includedFile).isFile()) { matches.put(includedFile, new BuildJob(includedFile, type)); } else { matches.put(includedDir, new BuildJob(includedDir, type)); } } return matches.values().toArray(new BuildJob[matches.size()]); } /** * Relativizes the project paths of the specified build jobs against the directory specified by * {@link #projectsDirectory} (if possible). If a project path does not denote a sub path of the projects directory, * it is returned as is. * * @param buildJobs The build jobs whose project paths should be relativized, must not be <code>null</code> nor * contain <code>null</code> elements. * @throws java.io.IOException If any path could not be relativized. */ private void relativizeProjectPaths(BuildJob[] buildJobs) throws IOException { String projectsDirPath = projectsDirectory.getCanonicalPath(); for (BuildJob buildJob : buildJobs) { String projectPath = buildJob.getProject(); File file = new File(projectPath); if (!file.isAbsolute()) { file = new File(projectsDirectory, projectPath); } String relativizedPath = relativizePath(file, projectsDirPath); if (relativizedPath == null) { relativizedPath = projectPath; } buildJob.setProject(relativizedPath); } } /** * Relativizes the specified path against the given base directory. Besides relativization, the returned path will * also be normalized, e.g. directory references like ".." will be removed. * * @param path The path to relativize, must not be <code>null</code>. * @param basedir The (canonical path of the) base directory to relativize against, must not be <code>null</code>. * @return The relative path in normal form or <code>null</code> if the input path does not denote a sub path of the * base directory. * @throws java.io.IOException If the path could not be relativized. */ private String relativizePath(File path, String basedir) throws IOException { String relativizedPath = path.getCanonicalPath(); if (relativizedPath.startsWith(basedir)) { relativizedPath = relativizedPath.substring(basedir.length()); if (relativizedPath.startsWith(File.separator)) { relativizedPath = relativizedPath.substring(File.separator.length()); } return relativizedPath; } else { return null; } } /** * Returns the map-based value source used to interpolate POMs and other stuff. * * @return The map-based value source for interpolation, never <code>null</code>. */ private Map<String, Object> getInterpolationValueSource() { Map<String, Object> props = new HashMap<String, Object>(); if (interpolationsProperties != null) { props.putAll((Map) interpolationsProperties); } if (filterProperties != null) { props.putAll(filterProperties); } props.put("basedir", this.project.getBasedir().getAbsolutePath()); props.put("baseurl", toUrl(this.project.getBasedir().getAbsolutePath())); if (settings.getLocalRepository() != null) { props.put("localRepository", settings.getLocalRepository()); props.put("localRepositoryUrl", toUrl(settings.getLocalRepository())); } return new CompositeMap(this.project, props); } /** * Converts the specified filesystem path to a URL. The resulting URL has no trailing slash regardless whether the * path denotes a file or a directory. * * @param filename The filesystem path to convert, must not be <code>null</code>. * @return The <code>file:</code> URL for the specified path, never <code>null</code>. */ private static String toUrl(String filename) { /* * NOTE: Maven fails to properly handle percent-encoded "file:" URLs (WAGON-111) so don't use File.toURI() here * as-is but use the decoded path component in the URL. */ String url = "file://" + new File(filename).toURI().getPath(); if (url.endsWith("/")) { url = url.substring(0, url.length() - 1); } return url; } /** * Gets goal/profile names for the specified project, either directly from the plugin configuration or from an * external token file. * * @param basedir The base directory of the test project, must not be <code>null</code>. * @param filename The (simple) name of an optional file in the project base directory from which to read * goals/profiles, may be <code>null</code>. * @param defaultTokens The list of tokens to return in case the specified token file does not exist, may be * <code>null</code>. * @return The list of goal/profile names, may be empty but never <code>null</code>. * @throws java.io.IOException If the token file exists but could not be parsed. */ private List<String> getTokens(File basedir, String filename, List<String> defaultTokens) throws IOException { List<String> tokens = (defaultTokens != null) ? defaultTokens : new ArrayList<String>(); if (StringUtils.isNotEmpty(filename)) { File tokenFile = new File(basedir, filename); if (tokenFile.exists()) { tokens = readTokens(tokenFile); } } return tokens; } /** * Reads the tokens from the specified file. Tokens are separated either by line terminators or commas. During * parsing, the file contents will be interpolated. * * @param tokenFile The file to read the tokens from, must not be <code>null</code>. * @return The list of tokens, may be empty but never <code>null</code>. * @throws java.io.IOException If the token file could not be read. */ private List<String> readTokens(final File tokenFile) throws IOException { List<String> result = new ArrayList<String>(); BufferedReader reader = null; try { Map<String, Object> composite = getInterpolationValueSource(); reader = new BufferedReader(new InterpolationFilterReader(newReader(tokenFile), composite)); String line; while ((line = reader.readLine()) != null) { result.addAll(collectListFromCSV(line)); } } finally { IOUtil.close(reader); } return result; } /** * Gets a list of comma separated tokens from the specified line. * * @param csv The line with comma separated tokens, may be <code>null</code>. * @return The list of tokens from the line, may be empty but never <code>null</code>. */ private List<String> collectListFromCSV(final String csv) { final List<String> result = new ArrayList<String>(); if ((csv != null) && (csv.trim().length() > 0)) { final StringTokenizer st = new StringTokenizer(csv, ","); while (st.hasMoreTokens()) { result.add(st.nextToken().trim()); } } return result; } /** * Interpolates the specified POM/settings file to a temporary file. The destination file may be same as the input * file, i.e. interpolation can be performed in-place. * * @param originalFile The XML file to interpolate, must not be <code>null</code>. * @param interpolatedFile The target file to write the interpolated contents of the original file to, must not be * <code>null</code>. * @throws org.apache.maven.plugin.MojoExecutionException * If the target file could not be created. */ void buildInterpolatedFile(File originalFile, File interpolatedFile) throws MojoExecutionException { getLog().debug("Interpolate " + originalFile.getPath() + " to " + interpolatedFile.getPath()); try { String xml; Reader reader = null; try { // interpolation with token @...@ Map<String, Object> composite = getInterpolationValueSource(); reader = ReaderFactory.newXmlReader(originalFile); reader = new InterpolationFilterReader(reader, composite, "@", "@"); xml = IOUtil.toString(reader); } finally { IOUtil.close(reader); } Writer writer = null; try { interpolatedFile.getParentFile().mkdirs(); writer = WriterFactory.newXmlWriter(interpolatedFile); writer.write(xml); writer.flush(); } finally { IOUtil.close(writer); } } catch (IOException e) { throw new MojoExecutionException("Failed to interpolate file " + originalFile.getPath(), e); } } /** * Gets the (interpolated) invoker properties for an integration test. * * @param projectDirectory The base directory of the IT project, must not be <code>null</code>. * @return The invoker properties, may be empty but never <code>null</code>. * @throws org.apache.maven.plugin.MojoExecutionException * If an I/O error occurred during reading the properties. */ private InvokerProperties getInvokerProperties(final File projectDirectory) throws MojoExecutionException { Properties props = new Properties(); if (invokerPropertiesFile != null) { File propertiesFile = new File(projectDirectory, invokerPropertiesFile); if (propertiesFile.isFile()) { InputStream in = null; try { in = new FileInputStream(propertiesFile); props.load(in); } catch (IOException e) { throw new MojoExecutionException("Failed to read invoker properties: " + propertiesFile, e); } finally { IOUtil.close(in); } } Interpolator interpolator = new RegexBasedInterpolator(); interpolator.addValueSource(new MapBasedValueSource(getInterpolationValueSource())); for (String key : (Set<String>) ((Map) props).keySet()) { String value = props.getProperty(key); try { value = interpolator.interpolate(value, ""); } catch (InterpolationException e) { throw new MojoExecutionException("Failed to interpolate invoker properties: " + propertiesFile, e); } props.setProperty(key, value); } } return new InvokerProperties(props); } protected boolean isParallelRun() { return parallelThreads > 1; } }