+ * As of 3.2.0 it is possible to put this folder in any of the ancestor folders, where properties will be inherited.
+ * This way you can provide a single properties file for a group of projects
+ *
+ *
+ * The snippet below describes the supported properties:
+ *
+ * # 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.
+ * # Environment variables used by maven plugins can be added here
+ * invoker.goals = clean install -Dplugin.variable=value
+ *
+ * # Or you can give things like this if you need.
+ * invoker.goals = -T2 clean verify
+ *
+ * # 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 mapping for toolchain to ensure it exists
+ * # Since plugin version 3.2.0
+ * invoker.toolchain.<type>.<provides> = value
+ * invoker.toolchain.jdk.version = 11
+ *
+ * # For java.version, maven.version, os.family and toolchain it is possible to define multiple selectors.
+ * # If one of the indexed selectors matches, the test is executed.
+ * # With the invoker.x.y equivalents you can specify global matchers.
+ * selector.1.java.version = 1.8+
+ * selector.1.maven.version = 3.2.5+
+ * selector.1.os.family = !windows
+ * selector.2.maven.version = 3.0+
+ * selector.3.java.version = 9+
+ *
+ * # A boolean value controlling the debug logging level of Maven, , defaults to "false"
+ * # Since plugin version 1.8
+ * invoker.debug = true
+ *
+ * # Path to an alternate settings.xml to use for Maven invocation with this IT.
+ * # Since plugin version 3.0.1
+ * invoker.settingsFile = ../
+ *
+ * # An integer value to control run order of projects. sorted in the descending order of the ordinal.
+ * # In other words, the BuildJobs with the highest numbers will be executed first
+ * # Since plugin version 3.2.1
+ * invoker.ordinal = 3
+ * invoker.ordinal = 1
+ *
+ * # The additional value for the environment variable.
+ * # Since plugin version 3.2.2
+ * invoker.environmentVariables.<variableName> = variableValue
+ * invoker.environmentVariables.MY_ENV_NAME = myEnvValue
+ *
+ *
+ *
+ * @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.
+ * When terminated with "C", the number part is multiplied by the number of processors (cores) available
+ * to the Java virtual machine. Floating point value are only accepted together with "C".
+ *
+ *
Example values: "1.5C", "4"
+ *
+ * @since 1.6
+ */
+ @Parameter( property = "invoker.parallelThreads", defaultValue = "1" )
+ private String parallelThreads;
+
+ /**
+ * @since 1.6
+ */
+ @Parameter( property = "plugin.artifacts", required = true, readonly = true )
+ private List 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 environmentVariables;
+
+ /**
+ * Additional variables for use in the hook scripts.
+ *
+ * @since 1.9
+ */
+ @Parameter
+ private Map scriptVariables;
+
+ /**
+ *
+ * @since 3.0.2
+ */
+ @Parameter( defaultValue = "0", property = "invoker.timeoutInSeconds" )
+ private int timeoutInSeconds;
+
+ /**
+ * Write test result in junit format.
+ * @since 3.1.2
+ */
+ @Parameter( defaultValue = "false", property = "invoker.writeJunitReport" )
+ private boolean writeJunitReport;
+
+ /**
+ * The package name use in junit report
+ * @since 3.1.2
+ */
+ @Parameter( defaultValue = "maven.invoker.it", property = "invoker.junitPackageName" )
+ private String junitPackageName = "maven.invoker.it";
+
+ /**
+ * 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 null. This will be set to
+ * null 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;
+
+ /**
+ * 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;
+ }
+
+ if ( StringUtils.isEmpty( encoding ) )
+ {
+ getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
+ + ", i.e. build is platform dependent!" );
+ }
+
+ // done it here to prevent issues with concurrent access in case of parallel run
+ if ( !disableReports )
+ {
+ setupReportsFolder();
+ }
+
+ List buildJobs;
+ if ( pom == null )
+ {
+ try
+ {
+ buildJobs = getBuildJobs();
+ }
+ catch ( final IOException e )
+ {
+ throw new MojoExecutionException( "Error retrieving POM list from includes, "
+ + "excludes, and projects directory. Reason: " + e.getMessage(), e );
+ }
+ }
+ else
+ {
+ try
+ {
+ projectsDirectory = pom.getCanonicalFile().getParentFile();
+ }
+ catch ( IOException e )
+ {
+ throw new MojoExecutionException( "Failed to discover projectsDirectory from "
+ + "pom File parameter. Reason: " + e.getMessage(), e );
+ }
+
+ buildJobs = Collections.singletonList( new BuildJob( pom.getName(), BuildJob.Type.NORMAL ) );
+ }
+
+ if ( buildJobs.isEmpty() )
+ {
+ doFailIfNoProjects();
+
+ getLog().info( "No projects were selected for execution." );
+ return;
+ }
+
+ handleScriptRunnerWithScriptClassPath();
+
+ Collection collectedProjects = new LinkedHashSet<>();
+ for ( BuildJob buildJob : buildJobs )
+ {
+ collectProjects( projectsDirectory, buildJob.getProject(), collectedProjects, true );
+ }
+
+ File projectsDir = projectsDirectory;
+
+ if ( cloneProjectsTo != null )
+ {
+ cloneProjects( collectedProjects );
+ projectsDir = cloneProjectsTo;
+ }
+ else if ( cloneProjectsTo == null && "maven-plugin".equals( project.getPackaging() ) )
+ {
+ cloneProjectsTo = new File( project.getBuild().getDirectory(), "its" );
+ cloneProjects( collectedProjects );
+ projectsDir = cloneProjectsTo;
+ }
+ else
+ {
+ getLog().warn( "Filtering of parent/child POMs is not supported without cloning the projects" );
+ }
+
+ // First run setup jobs.
+ List setupBuildJobs = null;
+ try
+ {
+ setupBuildJobs = getSetupBuildJobsFromFolders();
+ }
+ catch ( IOException e )
+ {
+ getLog().error( "Failure during scanning of folders.", e );
+ // TODO: Check shouldn't we fail in case of problems?
+ }
+
+ if ( !setupBuildJobs.isEmpty() )
+ {
+ // Run setup jobs in single thread
+ // mode.
+ //
+ // Some Idea about ordering?
+ getLog().info( "Running " + setupBuildJobs.size() + " setup job"
+ + ( ( setupBuildJobs.size() < 2 ) ? "" : "s" ) + ":" );
+ runBuilds( projectsDir, setupBuildJobs, 1 );
+ getLog().info( "Setup done." );
+ }
+
+ // Afterwards run all other jobs.
+ List nonSetupBuildJobs = getNonSetupJobs( buildJobs );
+ // We will run the non setup jobs with the configured
+ // parallelThreads number.
+ runBuilds( projectsDir, nonSetupBuildJobs, getParallelThreadsCount() );
+
+ writeSummaryFile( nonSetupBuildJobs );
+
+ processResults( new InvokerSession( nonSetupBuildJobs ) );
+
+ }
+
+ /**
+ * This will create the necessary folders for the reports.
+ *
+ * @throws MojoExecutionException in case of failure during creation of the reports folder.
+ */
+ private void setupReportsFolder()
+ throws MojoExecutionException
+ {
+ // If it exists from previous run...
+ if ( reportsDirectory.exists() )
+ {
+ try
+ {
+ FileUtils.deleteDirectory( reportsDirectory );
+ }
+ catch ( IOException e )
+ {
+ throw new MojoExecutionException( "Failure while trying to delete "
+ + reportsDirectory.getAbsolutePath(), e );
+ }
+ }
+ if ( !reportsDirectory.mkdirs() )
+ {
+ throw new MojoExecutionException( "Failure while creating the " + reportsDirectory.getAbsolutePath() );
+ }
+ }
+
+ private List getNonSetupJobs( List buildJobs )
+ {
+ List result = new LinkedList<>();
+ for ( BuildJob buildJob : buildJobs )
+ {
+ if ( !buildJob.getType().equals( BuildJob.Type.SETUP ) )
+ {
+ result.add( buildJob );
+ }
+ }
+ return result;
+ }
+
+ private void handleScriptRunnerWithScriptClassPath()
+ {
+ final List scriptClassPath;
+ if ( addTestClassPath )
+ {
+ scriptClassPath = new ArrayList<>( 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 entry : scriptVariables.entrySet() )
+ {
+ scriptRunner.setGlobalVariable( entry.getKey(), entry.getValue() );
+ }
+ }
+ scriptRunner.setClassPath( scriptClassPath );
+ }
+
+ private void writeSummaryFile( List buildJobs )
+ throws MojoExecutionException
+ {
+
+ File summaryReportFile = new File( reportsDirectory, "invoker-summary.txt" );
+
+ try ( Writer writer = new BufferedWriter( new FileWriter( summaryReportFile ) ) )
+ {
+ for ( BuildJob buildJob : buildJobs )
+ {
+ if ( !buildJob.getResult().equals( BuildJob.Result.SUCCESS ) )
+ {
+ writer.append( buildJob.getResult() );
+ writer.append( " [" );
+ writer.append( buildJob.getProject() );
+ writer.append( "] " );
+ if ( buildJob.getFailureMessage() != null )
+ {
+ writer.append( " " );
+ writer.append( buildJob.getFailureMessage() );
+ }
+ writer.append( "\n" );
+ }
+ }
+ }
+ catch ( IOException e )
+ {
+ throw new MojoExecutionException( "Failed to write summary report " + summaryReportFile, e );
+ }
+ }
+
+ 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 null.
+ * @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 null.
+ * @return The reader for the file, never null.
+ * @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 null.
+ * @param projectPath The relative path of the current project, can denote either the POM or its base directory,
+ * must not be null.
+ * @param projectPaths The set of already collected projects to add new projects to, must not be null.
+ * 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 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 modulePaths = new LinkedHashSet<>();
+
+ 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
+ * null nor contain null elements.
+ * @throws org.apache.maven.plugin.MojoExecutionException If the the projects could not be copied/filtered.
+ */
+ private void cloneProjects( Collection 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 dirs = new LinkedHashSet<>();
+ 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 clonedSubpaths = new ArrayList<>();
+
+ 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 );
+ }
+
+ // MINVOKER-186
+ // The following is a temporary solution to support Maven 3.3.1 (.mvn/extensions.xml) filtering
+ // Will be replaced by MINVOKER-117 with general filtering mechanism
+ File baseDir = pomFile.getParentFile();
+ File mvnDir = new File( baseDir, ".mvn" );
+ if ( mvnDir.isDirectory() )
+ {
+ File extensionsFile = new File( mvnDir, "extensions.xml" );
+ if ( extensionsFile.isFile() )
+ {
+ buildInterpolatedFile( extensionsFile, extensionsFile );
+ }
+ }
+ // END MINVOKER-186
+ }
+ filteredPomPrefix = null;
+ }
+ }
+
+ /**
+ * Gets the parent path of the specified relative path.
+ *
+ * @param path The relative path whose parent should be retrieved, must not be null.
+ * @return The parent path or "." if the specified path has no parent, never null.
+ */
+ 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 default exclusions (.svn, CVS, etc)
+ *
+ * @param sourceDir The source directory to copy, must not be null.
+ * @param destDir The target directory to copy to, must not be null.
+ * @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();
+ // Create all the directories, including any symlinks present in source
+ FileUtils.mkDirs( sourceDir, scanner.getIncludedDirectories(), destDir );
+
+ for ( String includedFile : scanner.getIncludedFiles() )
+ {
+ File sourceFile = new File( sourceDir, includedFile );
+ File destFile = new File( destDir, includedFile );
+ FileUtils.copyFile( sourceFile, destFile );
+
+ // ensure clone project must be writable for additional changes
+ destFile.setWritable( true );
+ }
+ }
+
+ /**
+ * 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 null.
+ * @param clonedSubpaths The list of already cloned paths, must not be null nor contain
+ * null elements.
+ * @return true if the specified path has already been cloned, false otherwise.
+ */
+ static boolean alreadyCloned( String subpath, List 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 null.
+ * @param buildJobs The build jobs to run must not be null nor contain null elements.
+ * @throws org.apache.maven.plugin.MojoExecutionException If any build could not be launched.
+ */
+ private void runBuilds( final File projectsDir, List buildJobs, int runWithParallelThreads )
+ throws MojoExecutionException
+ {
+ if ( !localRepositoryPath.exists() )
+ {
+ localRepositoryPath.mkdirs();
+ }
+
+ // -----------------------------------------------
+ // interpolate settings file
+ // -----------------------------------------------
+
+ File interpolatedSettingsFile = interpolateSettings( settingsFile );
+
+ final File mergedSettingsFile = mergeSettings( interpolatedSettingsFile );
+
+ if ( mavenHome != null )
+ {
+ actualMavenVersion = SelectorUtils.getMavenVersion( mavenHome );
+ }
+ else
+ {
+ actualMavenVersion = SelectorUtils.getMavenVersion();
+ }
+ scriptRunner.setGlobalVariable( "mavenVersion", actualMavenVersion );
+
+ final CharSequence actualJreVersion;
+ // @todo if ( javaVersions ) ... to be picked up from toolchains
+ if ( javaHome != null )
+ {
+ actualJreVersion = resolveExternalJreVersion();
+ }
+ else
+ {
+ actualJreVersion = SelectorUtils.getJreVersion();
+ }
+
+ final Path projectsPath = this.projectsDirectory.toPath();
+
+ Set folderGroupSet = new HashSet<>();
+ folderGroupSet.add( Paths.get( "." ) );
+ for ( BuildJob buildJob : buildJobs )
+ {
+ Path p = Paths.get( buildJob.getProject() );
+
+ if ( Files.isRegularFile( projectsPath.resolve( p ) ) )
+ {
+ p = p.getParent();
+ }
+
+ if ( p != null )
+ {
+ p = p.getParent();
+ }
+
+ while ( p != null && folderGroupSet.add( p ) )
+ {
+ p = p.getParent();
+ }
+ }
+
+ List folderGroup = new ArrayList<>( folderGroupSet );
+ Collections.sort( folderGroup );
+
+ final Map globalInvokerProperties = new HashMap<>();
+
+ for ( Path path : folderGroup )
+ {
+ Properties ancestorProperties = globalInvokerProperties.get( projectsPath.resolve( path ).getParent() );
+
+ Path currentInvokerProperties = projectsPath.resolve( path ).resolve( invokerPropertiesFile );
+
+ Properties currentProperties;
+ if ( Files.isRegularFile( currentInvokerProperties ) )
+ {
+ if ( ancestorProperties != null )
+ {
+ currentProperties = new Properties( ancestorProperties );
+
+ }
+ else
+ {
+ currentProperties = new Properties();
+ }
+ }
+ else
+ {
+ currentProperties = ancestorProperties;
+ }
+
+ if ( Files.isRegularFile( currentInvokerProperties ) )
+ {
+ try ( InputStream in = new FileInputStream( currentInvokerProperties.toFile() ) )
+ {
+ currentProperties.load( in );
+ }
+ catch ( IOException e )
+ {
+ throw new MojoExecutionException( "Failed to read invoker properties: "
+ + currentInvokerProperties );
+ }
+ }
+
+ if ( currentProperties != null )
+ {
+ globalInvokerProperties.put( projectsPath.resolve( path ).normalize(), currentProperties );
+ }
+ }
+
+ try
+ {
+ if ( runWithParallelThreads > 1 )
+ {
+ getLog().info( "use parallelThreads " + runWithParallelThreads );
+
+ ExecutorService executorService = Executors.newFixedThreadPool( runWithParallelThreads );
+ for ( final BuildJob job : buildJobs )
+ {
+ executorService.execute( new Runnable()
+ {
+ public void run()
+ {
+ try
+ {
+ Path ancestorFolder = getAncestorFolder( projectsPath.resolve( job.getProject() ) );
+
+ runBuild( projectsDir, job, mergedSettingsFile, javaHome, actualJreVersion,
+ globalInvokerProperties.get( ancestorFolder ) );
+ }
+ 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 )
+ {
+ Path ancestorFolder = getAncestorFolder( projectsPath.resolve( job.getProject() ) );
+
+ runBuild( projectsDir, job, mergedSettingsFile, javaHome, actualJreVersion,
+ globalInvokerProperties.get( ancestorFolder ) );
+ }
+ }
+ }
+ finally
+ {
+ if ( interpolatedSettingsFile != null && cloneProjectsTo == null )
+ {
+ interpolatedSettingsFile.delete();
+ }
+ if ( mergedSettingsFile != null && mergedSettingsFile.exists() )
+ {
+ mergedSettingsFile.delete();
+ }
+ }
+ }
+
+ private Path getAncestorFolder( Path p )
+ {
+ Path ancestor = p;
+ if ( Files.isRegularFile( ancestor ) )
+ {
+ ancestor = ancestor.getParent();
+ }
+ if ( ancestor != null )
+ {
+ ancestor = ancestor.getParent();
+ }
+ return ancestor;
+ }
+
+ /**
+ * Interpolate settings.xml file.
+ * @param settingsFile a settings file
+ *
+ * @return The interpolated settings.xml file.
+ * @throws MojoExecutionException in case of a problem.
+ */
+ private File interpolateSettings( File settingsFile )
+ throws MojoExecutionException
+ {
+ 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 );
+ }
+ return interpolatedSettingsFile;
+ }
+
+ /**
+ * Merge the settings file
+ *
+ * @param interpolatedSettingsFile The interpolated settings file.
+ * @return The merged settings file.
+ * @throws MojoExecutionException Fail the build in case the merged settings file can't be created.
+ */
+ private File mergeSettings( File interpolatedSettingsFile )
+ throws MojoExecutionException
+ {
+ 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
+ try
+ {
+ SettingsBuildingRequest request = new DefaultSettingsBuildingRequest();
+ request.setGlobalSettingsFile( interpolatedSettingsFile );
+
+ Settings dominantSettings = settingsBuilder.build( request ).getEffectiveSettings();
+ Settings recessiveSettings = cloneSettings();
+ SettingsUtils.merge( dominantSettings, recessiveSettings, TrackableBase.USER_LEVEL );
+
+ mergedSettings = dominantSettings;
+ getLog().debug( "Merged specified settings file with settings of invoking process" );
+ }
+ catch ( SettingsBuildingException e )
+ {
+ throw new MojoExecutionException( "Could not read specified settings file", e );
+ }
+ }
+ }
+
+ if ( this.settingsFile != null && !mergeUserSettings )
+ {
+ mergedSettingsFile = interpolatedSettingsFile;
+ }
+ else
+ {
+ try
+ {
+ mergedSettingsFile = writeMergedSettingsFile( mergedSettings );
+ }
+ catch ( IOException e )
+ {
+ throw new MojoExecutionException( "Could not create temporary file for invoker settings.xml", e );
+ }
+ }
+ return mergedSettingsFile;
+ }
+
+ private File writeMergedSettingsFile( Settings mergedSettings )
+ throws IOException
+ {
+ File mergedSettingsFile;
+ mergedSettingsFile = File.createTempFile( "invoker-settings", ".xml" );
+
+ SettingsXpp3Writer settingsWriter = new SettingsXpp3Writer();
+
+
+ try ( FileWriter fileWriter = new FileWriter( mergedSettingsFile ) )
+ {
+ settingsWriter.write( fileWriter, mergedSettings );
+ }
+
+ if ( getLog().isDebugEnabled() )
+ {
+ getLog().debug( "Created temporary file for invoker settings.xml: "
+ + mergedSettingsFile.getAbsolutePath() );
+ }
+ return mergedSettingsFile;
+ }
+
+ 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 CharSequence 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" );
+
+ final StringBuilder actualJreVersion = new StringBuilder();
+ StreamConsumer consumer = new StreamConsumer()
+ {
+ public void consumeLine( String line )
+ {
+ actualJreVersion.append( line );
+ }
+ };
+ try
+ {
+ CommandLineUtils.executeCommandLine( commandLine, consumer, null );
+ }
+ catch ( CommandLineException e )
+ {
+ getLog().warn( e.getMessage() );
+ }
+ return actualJreVersion;
+ }
+
+ /**
+ * Interpolate the pom file.
+ *
+ * @param pomFile The pom file.
+ * @param basedir The base directory.
+ * @return interpolated pom file location in case we have interpolated the pom file otherwise the original pom file
+ * will be returned.
+ * @throws MojoExecutionException
+ */
+ private File interpolatePomFile( File pomFile, File basedir )
+ throws MojoExecutionException
+ {
+ File interpolatedPomFile = null;
+ if ( pomFile != null )
+ {
+ if ( StringUtils.isNotEmpty( filteredPomPrefix ) )
+ {
+ interpolatedPomFile = new File( basedir, filteredPomPrefix + pomFile.getName() );
+ buildInterpolatedFile( pomFile, interpolatedPomFile );
+ }
+ else
+ {
+ interpolatedPomFile = pomFile;
+ }
+ }
+ return interpolatedPomFile;
+ }
+
+ /**
+ * Runs the specified project.
+ *
+ * @param projectsDir The base directory of all projects, must not be null.
+ * @param buildJob The build job to run, must not be null.
+ * @param settingsFile The (already interpolated) user settings file for the build, may be null to use
+ * the current user settings.
+ * @param globalInvokerProperties
+ * @throws org.apache.maven.plugin.MojoExecutionException If the project could not be launched.
+ */
+ private void runBuild( File projectsDir, BuildJob buildJob, File settingsFile, File actualJavaHome,
+ CharSequence actualJreVersion, Properties globalInvokerProperties )
+ throws MojoExecutionException
+ {
+ // FIXME: Think about the following code part -- START
+ 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();
+ }
+
+ File interpolatedPomFile = interpolatePomFile( pomFile, basedir );
+ // FIXME: Think about the following code part -- ^^^^^^^ END
+
+ getLog().info( buffer().a( "Building: " ).strong( buildJob.getProject() ).toString() );
+
+ InvokerProperties invokerProperties = getInvokerProperties( basedir, globalInvokerProperties );
+
+ // let's set what details we can
+ buildJob.setName( invokerProperties.getJobName() );
+ buildJob.setDescription( invokerProperties.getJobDescription() );
+
+ try
+ {
+ int selection = getSelection( invokerProperties, actualJreVersion );
+ if ( selection == 0 )
+ {
+ long milliseconds = System.currentTimeMillis();
+ boolean executed;
+
+ FileLogger buildLogger = setupBuildLogFile( basedir );
+ if ( buildLogger != null )
+ {
+ buildJob.setBuildlog( buildLogger.getOutputFile().getAbsolutePath() );
+ }
+
+ try
+ {
+ executed = runBuild( basedir, interpolatedPomFile, settingsFile, actualJavaHome,
+ invokerProperties, buildLogger );
+ }
+ finally
+ {
+ milliseconds = System.currentTimeMillis() - milliseconds;
+ buildJob.setTime( milliseconds / 1000.0 );
+
+ if ( buildLogger != null )
+ {
+ buildLogger.close();
+ }
+ }
+
+ if ( executed )
+ {
+ buildJob.setResult( BuildJob.Result.SUCCESS );
+
+ if ( !suppressSummaries )
+ {
+ getLog().info( pad( buildJob ).success( "SUCCESS" ).a( ' ' )
+ + formatTime( buildJob.getTime() ) );
+ }
+ }
+ else
+ {
+ buildJob.setResult( BuildJob.Result.SKIPPED );
+
+ if ( !suppressSummaries )
+ {
+ getLog().info( pad( buildJob ).warning( "SKIPPED" ).a( ' ' )
+ + formatTime( buildJob.getTime() ) );
+ }
+ }
+ }
+ else
+ {
+ buildJob.setResult( BuildJob.Result.SKIPPED );
+
+ StringBuilder message = new StringBuilder();
+ if ( selection == Selector.SELECTOR_MULTI )
+ {
+ message.append( "non-matching selectors" );
+ }
+ else
+ {
+ if ( ( selection & Selector.SELECTOR_MAVENVERSION ) != 0 )
+ {
+ message.append( "Maven version" );
+ }
+ if ( ( selection & Selector.SELECTOR_JREVERSION ) != 0 )
+ {
+ if ( message.length() > 0 )
+ {
+ message.append( ", " );
+ }
+ message.append( "JRE version" );
+ }
+ if ( ( selection & Selector.SELECTOR_OSFAMILY ) != 0 )
+ {
+ if ( message.length() > 0 )
+ {
+ message.append( ", " );
+ }
+ message.append( "OS" );
+ }
+ if ( ( selection & Selector.SELECTOR_TOOLCHAIN ) != 0 )
+ {
+ if ( message.length() > 0 )
+ {
+ message.append( ", " );
+ }
+ message.append( "Toolchain" );
+ }
+ }
+
+ if ( !suppressSummaries )
+ {
+ getLog().info( pad( buildJob ).warning( "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( " " + e.getMessage() );
+ getLog().info( pad( buildJob ).failure( "ERROR" ).a( ' ' ) + formatTime( buildJob.getTime() ) );
+ }
+ }
+ catch ( RunFailureException e )
+ {
+ buildJob.setResult( e.getType() );
+ buildJob.setFailureMessage( e.getMessage() );
+
+ if ( !suppressSummaries )
+ {
+ getLog().info( " " + e.getMessage() );
+ getLog().info( pad( buildJob ).failure( "FAILED" ).a( ' ' ) + formatTime( buildJob.getTime() ) );
+ }
+ }
+ finally
+ {
+ deleteInterpolatedPomFile( interpolatedPomFile );
+ writeBuildReport( buildJob );
+ }
+ }
+
+ private MessageBuilder pad( BuildJob buildJob )
+ {
+ MessageBuilder buffer = buffer( 128 );
+
+ buffer.a( " " );
+ buffer.a( buildJob.getProject() );
+
+ int l = 10 + buildJob.getProject().length();
+
+ if ( l < RESULT_COLUMN )
+ {
+ buffer.a( ' ' );
+ l++;
+
+ if ( l < RESULT_COLUMN )
+ {
+ for ( int i = RESULT_COLUMN - l; i > 0; i-- )
+ {
+ buffer.a( '.' );
+ }
+ }
+ }
+
+ return buffer.a( ' ' );
+ }
+
+ /**
+ * Delete the interpolated pom file if it has been created before.
+ *
+ * @param interpolatedPomFile The interpolated pom file.
+ */
+ private void deleteInterpolatedPomFile( File interpolatedPomFile )
+ {
+ if ( interpolatedPomFile != null && StringUtils.isNotEmpty( filteredPomPrefix ) )
+ {
+ interpolatedPomFile.delete();
+ }
+ }
+
+ /**
+ * Determines whether selector conditions of the specified invoker properties match the current environment.
+ *
+ * @param invokerProperties The invoker properties to check, must not be null.
+ * @return 0 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, CharSequence actualJreVersion )
+ {
+ return new Selector( actualMavenVersion, actualJreVersion.toString(),
+ getToolchainPrivateManager() ).getSelection( invokerProperties );
+ }
+
+ private ToolchainPrivateManager getToolchainPrivateManager()
+ {
+ return new ToolchainPrivateManager( toolchainManagerPrivate, session );
+ }
+
+ /**
+ * 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 null.
+ * @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 );
+ Writer osw = new OutputStreamWriter( fos, buildJob.getModelEncoding() ) )
+ {
+ BuildJobXpp3Writer writer = new BuildJobXpp3Writer();
+
+ writer.write( osw, buildJob );
+ }
+ catch ( IOException e )
+ {
+ throw new MojoExecutionException( "Failed to write build report " + reportFile, e );
+ }
+
+ if ( writeJunitReport )
+ {
+ writeJunitReport( buildJob, safeFileName );
+ }
+ }
+
+ private void writeJunitReport( BuildJob buildJob, String safeFileName )
+ throws MojoExecutionException
+ {
+ File reportFile = new File( reportsDirectory, "TEST-" + safeFileName + ".xml" );
+ Xpp3Dom testsuite = new Xpp3Dom( "testsuite" );
+ testsuite.setAttribute( "name", junitPackageName + "." + safeFileName );
+ testsuite.setAttribute( "time", Double.toString( buildJob.getTime() ) );
+
+ // set default value for required attributes
+ testsuite.setAttribute( "tests", "1" );
+ testsuite.setAttribute( "errors", "0" );
+ testsuite.setAttribute( "skipped", "0" );
+ testsuite.setAttribute( "failures", "0" );
+
+ Xpp3Dom testcase = new Xpp3Dom( "testcase" );
+ testsuite.addChild( testcase );
+ switch ( buildJob.getResult() )
+ {
+ case BuildJob.Result.SUCCESS:
+ break;
+ case BuildJob.Result.SKIPPED:
+ testsuite.setAttribute( "skipped", "1" );
+ // adding the failure element
+ Xpp3Dom skipped = new Xpp3Dom( "skipped" );
+ testcase.addChild( skipped );
+ skipped.setValue( buildJob.getFailureMessage() );
+ break;
+ case BuildJob.Result.ERROR:
+ testsuite.setAttribute( "errors", "1" );
+ break;
+ default:
+ testsuite.setAttribute( "failures", "1" );
+ // adding the failure element
+ Xpp3Dom failure = new Xpp3Dom( "failure" );
+ testcase.addChild( failure );
+ failure.setAttribute( "message", buildJob.getFailureMessage() );
+ }
+ testcase.setAttribute( "classname", junitPackageName + "." + safeFileName );
+ testcase.setAttribute( "name", safeFileName );
+ testcase.setAttribute( "time", Double.toString( buildJob.getTime() ) );
+ Xpp3Dom systemOut = new Xpp3Dom( "system-out" );
+ testcase.addChild( systemOut );
+
+
+ File buildLogFile = buildJob.getBuildlog() != null ? new File( buildJob.getBuildlog() ) : null;
+
+ if ( buildLogFile != null && buildLogFile.exists() )
+ {
+ getLog().debug( "fileLogger:" + buildLogFile );
+ try
+ {
+ systemOut.setValue( FileUtils.fileRead( buildLogFile ) );
+ }
+ catch ( IOException e )
+ {
+ throw new MojoExecutionException( "Failed to read logfile " + buildLogFile, e );
+ }
+ }
+ else
+ {
+ getLog().debug( safeFileName + "not exists buildLogFile = " + buildLogFile );
+ }
+
+ try ( FileOutputStream fos = new FileOutputStream( reportFile );
+ Writer osw = new OutputStreamWriter( fos, buildJob.getModelEncoding() ) )
+ {
+ Xpp3DomWriter.write( osw, testsuite );
+ } catch ( IOException e )
+ {
+ throw new MojoExecutionException( "Failed to write JUnit build report " + reportFile, e );
+ }
+ }
+
+ /**
+ * Formats the specified build duration time.
+ *
+ * @param seconds The duration of the build.
+ * @return The formatted time, never null.
+ */
+ private String formatTime( double seconds )
+ {
+ return secFormat.format( seconds );
+ }
+
+ /**
+ * Runs the specified project.
+ *
+ * @param basedir The base directory of the project, must not be null.
+ * @param pomFile The (already interpolated) POM file, may be null for a POM-less Maven invocation.
+ * @param settingsFile The (already interpolated) user settings file for the build, may be null. Will
+ * be merged with the settings file of the invoking Maven process.
+ * @param invokerProperties The properties to use.
+ * @param logger file logger to write execution build.log
+ * @return true if the project was launched or false 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, File actualJavaHome,
+ InvokerProperties invokerProperties, FileLogger logger )
+ throws MojoExecutionException, RunFailureException
+ {
+ if ( getLog().isDebugEnabled() && !invokerProperties.getProperties().isEmpty() )
+ {
+ Properties props = invokerProperties.getProperties();
+ getLog().debug( "Using invoker properties:" );
+ for ( String key : new TreeSet( props.stringPropertyNames() ) )
+ {
+ String value = props.getProperty( key );
+ getLog().debug( " " + key + " = " + value );
+ }
+ }
+
+ List goals = getGoals( basedir );
+
+ List profiles = getProfiles( basedir );
+
+ Map context = new LinkedHashMap<>();
+
+ boolean selectorResult = true;
+
+ try
+ {
+ try
+ {
+ scriptRunner.run( "selector script", basedir, selectorScript, context, logger, BuildJob.Result.SKIPPED,
+ false );
+ }
+ catch ( RunErrorException e )
+ {
+ selectorResult = false;
+ throw e;
+ }
+ catch ( RunFailureException e )
+ {
+ selectorResult = false;
+ 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.setBatchMode( true );
+
+ request.setShowErrors( showErrors );
+
+ request.setDebug( debug );
+
+ request.setShowVersion( showVersion );
+
+ setupLoggerForBuildJob( logger, request );
+
+ if ( mavenHome != null )
+ {
+ invoker.setMavenHome( mavenHome );
+ // FIXME: Should we really take care of M2_HOME?
+ request.addShellEnvironment( "M2_HOME", mavenHome.getAbsolutePath() );
+ }
+
+ if ( mavenExecutable != null )
+ {
+ invoker.setMavenExecutable( new File( mavenExecutable ) );
+ }
+
+ if ( actualJavaHome != null )
+ {
+ request.setJavaHome( actualJavaHome );
+ }
+
+ if ( environmentVariables != null )
+ {
+ for ( Map.Entry 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 );
+
+ int timeOut = invokerProperties.getTimeoutInSeconds( invocationIndex );
+ // not set so we use the one at the mojo level
+ request.setTimeoutInSeconds( timeOut < 0 ? timeoutInSeconds : timeOut );
+
+ String customSettingsFile = invokerProperties.getSettingsFile( invocationIndex );
+ if ( customSettingsFile != null )
+ {
+ File interpolateSettingsFile = interpolateSettings( new File( customSettingsFile ) );
+ File mergeSettingsFile = mergeSettings( interpolateSettingsFile );
+
+ request.setUserSettingsFile( mergeSettingsFile );
+ }
+ else
+ {
+ 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() );
+ }
+ }
+
+ try
+ {
+ InvocationResult result = invoker.execute( request );
+ verify( result, invocationIndex, invokerProperties, logger );
+ }
+ catch ( final MavenInvocationException e )
+ {
+ getLog().debug( "Error invoking Maven: " + e.getMessage(), e );
+ throw new RunFailureException( "Maven invocation failed. " + e.getMessage(),
+ BuildJob.Result.FAILURE_BUILD );
+ }
+ }
+ }
+ catch ( IOException e )
+ {
+ throw new MojoExecutionException( e.getMessage(), e );
+ }
+ finally
+ {
+ if ( selectorResult )
+ {
+ runPostBuildHook( basedir, context, logger );
+ }
+ }
+ return true;
+ }
+
+ int getParallelThreadsCount()
+ {
+ if ( parallelThreads.endsWith( "C" ) )
+ {
+ double parallelThreadsMultiple = Double.parseDouble(
+ parallelThreads.substring( 0, parallelThreads.length() - 1 ) );
+ return (int) ( parallelThreadsMultiple * Runtime.getRuntime().availableProcessors() );
+ }
+ else
+ {
+ return Integer.parseInt( parallelThreads );
+ }
+ }
+
+ private void runPostBuildHook( File basedir, Map context, FileLogger logger )
+ throws MojoExecutionException, RunFailureException
+ {
+ try
+ {
+ scriptRunner.run( "post-build script", basedir, postBuildHookScript, context, logger,
+ BuildJob.Result.FAILURE_POST_HOOK, true );
+ }
+ catch ( IOException e )
+ {
+ throw new MojoExecutionException( e.getMessage(), e );
+ }
+ }
+ private void setupLoggerForBuildJob( FileLogger logger, final InvocationRequest request )
+ {
+ if ( logger != null )
+ {
+ request.setErrorHandler( logger );
+
+ request.setOutputHandler( logger );
+ }
+ }
+
+ /**
+ * Initializes the build logger for the specified project. This will write the logging information into
+ * {@code build.log}.
+ *
+ * @param basedir The base directory of the project, must not be null.
+ * @return The build logger or null if logging has been disabled.
+ * @throws org.apache.maven.plugin.MojoExecutionException If the log file could not be created.
+ */
+ private FileLogger setupBuildLogFile( File basedir )
+ throws MojoExecutionException
+ {
+ FileLogger logger = null;
+
+ if ( !noLog )
+ {
+ Path projectLogDirectory;
+ if ( logDirectory == null )
+ {
+ projectLogDirectory = basedir.toPath();
+ }
+ else if ( cloneProjectsTo != null )
+ {
+ projectLogDirectory =
+ logDirectory.toPath().resolve( cloneProjectsTo.toPath().relativize( basedir.toPath() ) );
+ }
+ else
+ {
+ projectLogDirectory =
+ logDirectory.toPath().resolve( projectsDirectory.toPath().relativize( basedir.toPath() ) );
+ }
+
+ try
+ {
+ if ( streamLogs )
+ {
+ logger = new FileLogger( projectLogDirectory.resolve( "build.log" ).toFile(), getLog() );
+ }
+ else
+ {
+ logger = new FileLogger( projectLogDirectory.resolve( "build.log" ).toFile() );
+ }
+
+ getLog().debug( "Build log initialized in: " + projectLogDirectory );
+ }
+ catch ( IOException e )
+ {
+ throw new MojoExecutionException( "Error initializing build logfile in: " + projectLogDirectory, e );
+ }
+ }
+
+ return logger;
+ }
+
+ /**
+ * Gets the system properties to use for the specified project.
+ *
+ * @param basedir The base directory of the project, must not be null.
+ * @param filename The filename to the properties file to load, may be null to use the default path
+ * given by {@link #testPropertiesFile}.
+ * @return The system properties to use, may be empty but never null.
+ * @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 ( properties != null )
+ {
+ // MINVOKER-118: property can have empty value, which is not accepted by collectedTestProperties
+ for ( Map.Entry 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() )
+ {
+
+ try ( InputStream 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 );
+ }
+ }
+
+ return collectedTestProperties;
+ }
+
+ /**
+ * Verifies the invocation result.
+ *
+ * @param result The invocation result to check, must not be null.
+ * @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 null.
+ * @param logger The build logger, may be null 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 null.
+ * @return The list of goals to run when building the project, may be empty but never null.
+ * @throws org.apache.maven.plugin.MojoExecutionException If the profile file could not be read.
+ */
+ List getGoals( final File basedir )
+ throws MojoExecutionException
+ {
+ try
+ {
+ // FIXME: Currently we have null for goalsFile which has been removed.
+ // This might mean we can remove getGoals() at all ? Check this.
+ return getTokens( basedir, null, 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 null.
+ * @return The list of profiles to activate when building the project, may be empty but never null.
+ * @throws org.apache.maven.plugin.MojoExecutionException If the profile file could not be read.
+ */
+ List getProfiles( File basedir )
+ throws MojoExecutionException
+ {
+ try
+ {
+ return getTokens( basedir, null, profiles );
+ }
+ catch ( IOException e )
+ {
+ throw new MojoExecutionException( "error reading profiles", e );
+ }
+ }
+
+ private List calculateExcludes()
+ throws IOException
+ {
+ List excludes =
+ ( pomExcludes != null ) ? new ArrayList<>( pomExcludes ) : new ArrayList();
+ 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" );
+ }
+ }
+ return excludes;
+
+ }
+
+ /**
+ * @return The list of setupUp jobs.
+ * @throws IOException
+ * @see {@link #setupIncludes}
+ */
+ private List getSetupBuildJobsFromFolders()
+ throws IOException, MojoExecutionException
+ {
+ List excludes = calculateExcludes();
+
+ List setupPoms = scanProjectsDirectory( setupIncludes, excludes, BuildJob.Type.SETUP );
+ if ( getLog().isDebugEnabled() )
+ {
+ getLog().debug( "Setup projects: " + setupPoms );
+ }
+
+ return setupPoms;
+ }
+
+ private static class OrdinalComparator implements Comparator
+ {
+ private static final OrdinalComparator INSTANCE = new OrdinalComparator();
+
+ @Override
+ public int compare( Object o1, Object o2 )
+ {
+ return Integer.compare( ( ( BuildJob ) o2 ).getOrdinal(), ( ( BuildJob ) o1 ).getOrdinal() );
+ }
+ }
+
+ /**
+ * 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 null.
+ * @throws java.io.IOException If the projects directory could not be scanned.
+ */
+ List getBuildJobs()
+ throws IOException, MojoExecutionException
+ {
+ List buildJobs;
+
+ if ( invokerTest == null )
+ {
+ List excludes = calculateExcludes();
+
+ List setupPoms = scanProjectsDirectory( setupIncludes, excludes, BuildJob.Type.SETUP );
+ if ( getLog().isDebugEnabled() )
+ {
+ getLog().debug( "Setup projects: " + Arrays.asList( setupPoms ) );
+ }
+
+ List normalPoms = scanProjectsDirectory( pomIncludes, excludes, BuildJob.Type.NORMAL );
+
+ Map uniquePoms = new LinkedHashMap<>();
+ for ( BuildJob setupPom : setupPoms )
+ {
+ uniquePoms.put( setupPom.getProject(), setupPom );
+ }
+ for ( BuildJob normalPom : normalPoms )
+ {
+ if ( !uniquePoms.containsKey( normalPom.getProject() ) )
+ {
+ uniquePoms.put( normalPom.getProject(), normalPom );
+ }
+ }
+
+ buildJobs = new ArrayList<>( uniquePoms.values() );
+ }
+ else
+ {
+ String[] testRegexes = StringUtils.split( invokerTest, "," );
+ List includes = new ArrayList<>( testRegexes.length );
+ List excludes = new ArrayList<>();
+
+ 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 );
+ }
+
+ 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 null.
+ * @param excludes The exclude patterns for the scanner, may be null to exclude nothing.
+ * @param type The type to assign to the resulting build jobs, must not be null.
+ * @return The build jobs matching the patterns, never null.
+ * @throws java.io.IOException If the project directory could not be scanned.
+ */
+ private List scanProjectsDirectory( List includes, List excludes, String type )
+ throws IOException, MojoExecutionException
+ {
+ if ( !projectsDirectory.isDirectory() )
+ {
+ return Collections.emptyList();
+ }
+
+ 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 matches = new LinkedHashMap<>();
+
+ 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 ) );
+ }
+ }
+
+ List projects = new ArrayList<>( matches.size() );
+
+ // setup ordinal values to have an order here
+ for ( BuildJob buildJob : matches.values() )
+ {
+ InvokerProperties invokerProperties =
+ getInvokerProperties( new File( projectsDirectory, buildJob.getProject() ).getParentFile(),
+ null );
+ buildJob.setOrdinal( invokerProperties.getOrdinal() );
+ projects.add( buildJob );
+ }
+ Collections.sort( projects, OrdinalComparator.INSTANCE );
+ return projects;
+ }
+
+ /**
+ * 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 null nor
+ * contain null elements.
+ * @throws java.io.IOException If any path could not be relativized.
+ */
+ private void relativizeProjectPaths( List 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 null.
+ * @param basedir The (canonical path of the) base directory to relativize against, must not be null.
+ * @return The relative path in normal form or null 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.
+ *
+ * @param escapeXml {@code true}, to escape any XML special characters in the property values; {@code false}, to not
+ * escape any property values.
+ *
+ * @return The map-based value source for interpolation, never null.
+ */
+ private Map getInterpolationValueSource( final boolean escapeXml )
+ {
+ Map props = new HashMap<>();
+
+ 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, escapeXml );
+ }
+
+ /**
+ * 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 null.
+ * @return The file: URL for the specified path, never null.
+ */
+ 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 null.
+ * @param filename The (simple) name of an optional file in the project base directory from which to read
+ * goals/profiles, may be null.
+ * @param defaultTokens The list of tokens to return in case the specified token file does not exist, may be
+ * null.
+ * @return The list of goal/profile names, may be empty but never null.
+ * @throws java.io.IOException If the token file exists but could not be parsed.
+ */
+ private List getTokens( File basedir, String filename, List defaultTokens )
+ throws IOException
+ {
+ List tokens = ( defaultTokens != null ) ? defaultTokens : new ArrayList();
+
+ 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 null.
+ * @return The list of tokens, may be empty but never null.
+ * @throws java.io.IOException If the token file could not be read.
+ */
+ private List readTokens( final File tokenFile )
+ throws IOException
+ {
+ List result = new ArrayList<>();
+
+ Map composite = getInterpolationValueSource( false );
+
+ try ( BufferedReader reader =
+ new BufferedReader( new InterpolationFilterReader( newReader( tokenFile ), composite ) ) )
+ {
+ for ( String line = reader.readLine(); line != null; line = reader.readLine() )
+ {
+ result.addAll( collectListFromCSV( line ) );
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Gets a list of comma separated tokens from the specified line.
+ *
+ * @param csv The line with comma separated tokens, may be null.
+ * @return The list of tokens from the line, may be empty but never null.
+ */
+ private List collectListFromCSV( final String csv )
+ {
+ final List result = new ArrayList<>();
+
+ 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.
+ *
+ * Note:This methods expects the file to be a XML file and applies special XML escaping during interpolation.
+ *
+ *
+ * @param originalFile The XML file to interpolate, must not be null.
+ * @param interpolatedFile The target file to write the interpolated contents of the original file to, must not be
+ * null.
+ *
+ * @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;
+
+ Map composite = getInterpolationValueSource( true );
+
+ // interpolation with token @...@
+ try ( Reader reader =
+ new InterpolationFilterReader( ReaderFactory.newXmlReader( originalFile ), composite, "@", "@" ) )
+ {
+ xml = IOUtil.toString( reader );
+ }
+
+ try ( Writer writer = WriterFactory.newXmlWriter( interpolatedFile ) )
+ {
+ interpolatedFile.getParentFile().mkdirs();
+
+ writer.write( xml );
+ }
+ }
+ 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 null.
+ * @return The invoker properties, may be empty but never null.
+ * @throws org.apache.maven.plugin.MojoExecutionException If an I/O error occurred during reading the properties.
+ */
+ private InvokerProperties getInvokerProperties( final File projectDirectory, Properties globalInvokerProperties )
+ throws MojoExecutionException
+ {
+ Properties props;
+ if ( globalInvokerProperties != null )
+ {
+ props = new Properties( globalInvokerProperties );
+ }
+ else
+ {
+ props = new Properties();
+ }
+
+ File propertiesFile = new File( projectDirectory, invokerPropertiesFile );
+ if ( propertiesFile.isFile() )
+ {
+ try ( InputStream in = new FileInputStream( propertiesFile ) )
+ {
+ props.load( in );
+ }
+ catch ( IOException e )
+ {
+ throw new MojoExecutionException( "Failed to read invoker properties: " + propertiesFile, e );
+ }
+ }
+
+ Interpolator interpolator = new RegexBasedInterpolator();
+ interpolator.addValueSource( new MapBasedValueSource( getInterpolationValueSource( false ) ) );
+ // CHECKSTYLE_OFF: LineLength
+ for ( String key : props.stringPropertyNames() )
+ {
+ 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 );
+ }
+
+ static class ToolchainPrivateManager
+ {
+ private ToolchainManagerPrivate manager;
+
+ private MavenSession session;
+
+ ToolchainPrivateManager( ToolchainManagerPrivate manager, MavenSession session )
+ {
+ this.manager = manager;
+ this.session = session;
+ }
+
+ ToolchainPrivate[] getToolchainPrivates( String type ) throws MisconfiguredToolchainException
+ {
+ return manager.getToolchainsForType( type, session );
+ }
+ }
+}
diff --git a/Java-base/maven-invoker-plugin/src/src/main/java/org/apache/maven/plugins/invoker/CompositeMap.java b/Java-base/maven-invoker-plugin/src/src/main/java/org/apache/maven/plugins/invoker/CompositeMap.java
new file mode 100644
index 000000000..13b902c20
--- /dev/null
+++ b/Java-base/maven-invoker-plugin/src/src/main/java/org/apache/maven/plugins/invoker/CompositeMap.java
@@ -0,0 +1,258 @@
+package org.apache.maven.plugins.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.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.util.introspection.ReflectionValueExtractor;
+
+/**
+ * A map-like source to interpolate expressions.
+ *
+ * @author Olivier Lamy
+ * @since 1.1
+ */
+class CompositeMap
+ implements Map
+{
+
+ /**
+ * The Maven project from which to extract interpolated values, never null.
+ */
+ private MavenProject mavenProject;
+
+ /**
+ * The set of additional properties from which to extract interpolated values, never null.
+ */
+ private Map properties;
+
+ /**
+ * Flag indicating to escape XML special characters.
+ */
+ private final boolean escapeXml;
+
+ /**
+ * Creates a new interpolation source backed by the specified Maven project and some user-specified properties.
+ *
+ * @param mavenProject The Maven project from which to extract interpolated values, must not be null.
+ * @param properties The set of additional properties from which to extract interpolated values, may be
+ * null.
+ * @param escapeXml {@code true}, to escape any XML special characters; {@code false}, to not perform any escaping.
+ */
+ protected CompositeMap( MavenProject mavenProject, Map properties, boolean escapeXml )
+ {
+ if ( mavenProject == null )
+ {
+ throw new IllegalArgumentException( "no project specified" );
+ }
+ this.mavenProject = mavenProject;
+ this.properties = properties == null ? new HashMap() : properties;
+ this.escapeXml = escapeXml;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.util.Map#clear()
+ */
+ public void clear()
+ {
+ // nothing here
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.util.Map#containsKey(java.lang.Object)
+ */
+ public boolean containsKey( Object key )
+ {
+ if ( !( key instanceof String ) )
+ {
+ return false;
+ }
+
+ String expression = (String) key;
+ if ( expression.startsWith( "project." ) || expression.startsWith( "pom." ) )
+ {
+ try
+ {
+ Object evaluated = ReflectionValueExtractor.evaluate( expression, this.mavenProject );
+ if ( evaluated != null )
+ {
+ return true;
+ }
+ }
+ catch ( Exception e )
+ {
+ // uhm do we have to throw a RuntimeException here ?
+ }
+ }
+
+ return properties.containsKey( key ) || mavenProject.getProperties().containsKey( key );
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.util.Map#containsValue(java.lang.Object)
+ */
+ public boolean containsValue( Object value )
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.util.Map#entrySet()
+ */
+ public Set> entrySet()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.util.Map#get(java.lang.Object)
+ */
+ public Object get( Object key )
+ {
+ if ( !( key instanceof String ) )
+ {
+ return null;
+ }
+
+ Object value = null;
+ String expression = (String) key;
+ if ( expression.startsWith( "project." ) || expression.startsWith( "pom." ) )
+ {
+ try
+ {
+ Object evaluated = ReflectionValueExtractor.evaluate( expression, this.mavenProject );
+ if ( evaluated != null )
+ {
+ value = evaluated;
+ }
+ }
+ catch ( Exception e )
+ {
+ // uhm do we have to throw a RuntimeException here ?
+ }
+ }
+
+ if ( value == null )
+ {
+ value = properties.get( key );
+ }
+
+ if ( value == null )
+ {
+ value = this.mavenProject.getProperties().get( key );
+ }
+
+ if ( value != null && this.escapeXml )
+ {
+ value = value.toString().
+ replaceAll( "\"", """ ).
+ replaceAll( "<", "<" ).
+ replaceAll( ">", ">" ).
+ replaceAll( "&", "&" );
+
+ }
+
+ return value;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.util.Map#isEmpty()
+ */
+ public boolean isEmpty()
+ {
+ return this.mavenProject.getProperties().isEmpty() && this.properties.isEmpty();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.util.Map#keySet()
+ */
+ public Set keySet()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.util.Map#put(java.lang.Object, java.lang.Object)
+ */
+ public Object put( String key, Object value )
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.util.Map#putAll(java.util.Map)
+ */
+ public void putAll( Map extends String, ? extends Object> t )
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.util.Map#remove(java.lang.Object)
+ */
+ public Object remove( Object key )
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.util.Map#size()
+ */
+ public int size()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.util.Map#values()
+ */
+ public Collection