You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by rf...@apache.org on 2017/05/15 21:10:28 UTC

svn commit: r1795243 [3/4] - in /maven/plugins/trunk/maven-invoker-plugin/src: it/local-repo-url/src/it/project/ main/java/org/apache/maven/plugin/ main/java/org/apache/maven/plugins/ main/java/org/apache/maven/plugins/invoker/ main/mdo/ test/java/org/...

Added: maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/CompositeMap.java
URL: http://svn.apache.org/viewvc/maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/CompositeMap.java?rev=1795243&view=auto
==============================================================================
--- maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/CompositeMap.java (added)
+++ maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/CompositeMap.java Mon May 15 21:10:27 2017
@@ -0,0 +1,259 @@
+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
+ * @version $Id: CompositeMap.java 1784076 2017-02-23 00:35:39Z schulte $
+ */
+class CompositeMap
+    implements Map<String, Object>
+{
+
+    /**
+     * The Maven project from which to extract interpolated values, never <code>null</code>.
+     */
+    private MavenProject mavenProject;
+
+    /**
+     * The set of additional properties from which to extract interpolated values, never <code>null</code>.
+     */
+    private Map<String, Object> 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 <code>null</code>.
+     * @param properties The set of additional properties from which to extract interpolated values, may be
+     *            <code>null</code>.
+     * @param escapeXml {@code true}, to escape any XML special characters; {@code false}, to not perform any escaping.
+     */
+    protected CompositeMap( MavenProject mavenProject, Map<String, Object> properties, boolean escapeXml )
+    {
+        if ( mavenProject == null )
+        {
+            throw new IllegalArgumentException( "no project specified" );
+        }
+        this.mavenProject = mavenProject;
+        this.properties = properties == null ? new HashMap<String, Object>() : 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<Entry<String, Object>> 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( "\"", "&quot;" ).
+                replaceAll( "<", "&lt;" ).
+                replaceAll( ">", "&gt;" ).
+                replaceAll( "&", "&amp;" );
+
+        }
+
+        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<String> 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<Object> values()
+    {
+        throw new UnsupportedOperationException();
+    }
+}

Added: maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/FileLogger.java
URL: http://svn.apache.org/viewvc/maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/FileLogger.java?rev=1795243&view=auto
==============================================================================
--- maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/FileLogger.java (added)
+++ maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/FileLogger.java Mon May 15 21:10:27 2017
@@ -0,0 +1,62 @@
+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.io.File;
+import java.io.IOException;
+
+import org.apache.maven.plugin.logging.Log;
+import org.apache.maven.shared.invoker.InvocationOutputHandler;
+import org.apache.maven.shared.scriptinterpreter.ExecutionLogger;
+
+/**
+ * @version $Id: FileLogger.java 1671476 2015-04-06 03:53:29Z dantran $
+ */
+class FileLogger
+    extends org.apache.maven.shared.scriptinterpreter.FileLogger
+    implements InvocationOutputHandler, ExecutionLogger
+{
+
+    /**
+     * Creates a new logger that writes to the specified file.
+     *
+     * @param outputFile The path to the output file, must not be <code>null</code>.
+     * @throws IOException If the output file could not be created.
+     */
+    public FileLogger( File outputFile )
+        throws IOException
+    {
+        super( outputFile, null );
+    }
+
+    /**
+     * Creates a new logger that writes to the specified file and optionally mirrors messages to the given mojo logger.
+     *
+     * @param outputFile The path to the output file, must not be <code>null</code>.
+     * @param log The mojo logger to additionally output messages to, may be <code>null</code> if not used.
+     * @throws IOException If the output file could not be created.
+     */
+    public FileLogger( File outputFile, Log log )
+        throws IOException
+    {
+        super( outputFile, log );
+    }
+
+}

Added: maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InstallMojo.java
URL: http://svn.apache.org/viewvc/maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InstallMojo.java?rev=1795243&view=auto
==============================================================================
--- maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InstallMojo.java (added)
+++ maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InstallMojo.java Mon May 15 21:10:27 2017
@@ -0,0 +1,605 @@
+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.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.factory.ArtifactFactory;
+import org.apache.maven.artifact.repository.ArtifactRepository;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.model.Model;
+import org.apache.maven.model.Parent;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Component;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.ProjectBuildingRequest;
+import org.apache.maven.shared.artifact.install.ArtifactInstaller;
+import org.apache.maven.shared.dependencies.DefaultDependableCoordinate;
+import org.apache.maven.shared.dependencies.resolve.DependencyResolver;
+import org.apache.maven.shared.dependencies.resolve.DependencyResolverException;
+import org.apache.maven.shared.repository.RepositoryManager;
+import org.codehaus.plexus.util.FileUtils;
+
+/**
+ * Installs the project artifacts of the main build into the local repository as a preparation to run the sub projects.
+ * More precisely, all artifacts of the project itself, all its locally reachable parent POMs and all its dependencies
+ * from the reactor will be installed to the local repository.
+ *
+ * @since 1.2
+ * @author Paul Gier
+ * @author Benjamin Bentmann
+ * @version $Id: InstallMojo.java 1748000 2016-06-12 13:20:59Z rfscholte $
+ */
+// CHECKSTYLE_OFF: LineLength
+@Mojo( name = "install", defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST, requiresDependencyResolution = ResolutionScope.RUNTIME, threadSafe = true )
+// CHECKSTYLE_ON: LineLength
+public class InstallMojo
+    extends AbstractMojo
+{
+
+    /**
+     * Maven artifact install component to copy artifacts to the local repository.
+     */
+    @Component
+    private ArtifactInstaller installer;
+    
+    @Component
+    private RepositoryManager repositoryManager;
+
+    /**
+     * The component used to create artifacts.
+     */
+    @Component
+    private ArtifactFactory artifactFactory;
+
+    /**
+     */
+    @Parameter( property = "localRepository", required = true, readonly = true )
+    private ArtifactRepository localRepository;
+
+    /**
+     * The path to the local repository into which the project artifacts should be installed for the integration tests.
+     * If not set, the regular local repository will be used. To prevent soiling of your regular local repository with
+     * possibly broken artifacts, it is strongly recommended to use an isolated repository for the integration tests
+     * (e.g. <code>${project.build.directory}/it-repo</code>).
+     */
+    @Parameter( property = "invoker.localRepositoryPath", 
+                defaultValue = "${session.localRepository.basedir}", required = true )
+    private File localRepositoryPath;
+
+    /**
+     * The current Maven project.
+     */
+    @Parameter( defaultValue = "${project}", readonly = true, required = true )
+    private MavenProject project;
+
+    @Parameter( defaultValue = "${session}", readonly = true, required = true )
+    private MavenSession session;
+    
+    /**
+     * The set of Maven projects in the reactor build.
+     */
+    @Parameter( defaultValue = "${reactorProjects}", readonly = true )
+    private Collection<MavenProject> reactorProjects;
+
+    /**
+     * A flag used to disable the installation procedure. This is primarily intended for usage from the command line to
+     * occasionally adjust the build.
+     *
+     * @since 1.4
+     */
+    @Parameter( property = "invoker.skip", defaultValue = "false" )
+    private boolean skipInstallation;
+
+    /**
+     * The identifiers of already installed artifacts, used to avoid multiple installation of the same artifact.
+     */
+    private Collection<String> installedArtifacts;
+
+    /**
+     * The identifiers of already copied artifacts, used to avoid multiple installation of the same artifact.
+     */
+    private Collection<String> copiedArtifacts;
+
+    /**
+     * Extra dependencies that need to be installed on the local repository.<BR>
+     * Format:
+     *
+     * <pre>
+     * groupId:artifactId:version:type:classifier
+     * </pre>
+     *
+     * Examples:
+     *
+     * <pre>
+     * org.apache.maven.plugins:maven-clean-plugin:2.4:maven-plugin
+     * org.apache.maven.plugins:maven-clean-plugin:2.4:jar:javadoc
+     * </pre>
+     *
+     * If the type is 'maven-plugin' the plugin will try to resolve the artifact using plugin remote repositories,
+     * instead of using artifact remote repositories.
+     *
+     * @since 1.6
+     */
+    @Parameter
+    private String[] extraArtifacts;
+
+    /**
+     */
+    @Component
+    private DependencyResolver resolver;
+
+    private ProjectBuildingRequest projectBuildingRequest;
+
+    /**
+     * Performs this mojo's tasks.
+     *
+     * @throws MojoExecutionException If the artifacts could not be installed.
+     */
+    public void execute()
+        throws MojoExecutionException
+    {
+        if ( skipInstallation )
+        {
+            getLog().info( "Skipping artifact installation per configuration." );
+            return;
+        }
+
+        createTestRepository();
+
+        installedArtifacts = new HashSet<String>();
+        copiedArtifacts = new HashSet<String>();
+
+        installProjectDependencies( project, reactorProjects );
+        installProjectParents( project );
+        installProjectArtifacts( project );
+
+        installExtraArtifacts( extraArtifacts );
+    }
+
+    /**
+     * Creates the local repository for the integration tests. If the user specified a custom repository location, the
+     * custom repository will have the same identifier, layout and policies as the real local repository. That means
+     * apart from the location, the custom repository will be indistinguishable from the real repository such that its
+     * usage is transparent to the integration tests.
+     *
+     * @return The local repository for the integration tests, never <code>null</code>.
+     * @throws MojoExecutionException If the repository could not be created.
+     */
+    private void createTestRepository()
+        throws MojoExecutionException
+    {
+        
+        if ( !localRepositoryPath.exists() && !localRepositoryPath.mkdirs() )
+        {
+            throw new MojoExecutionException( "Failed to create directory: " + localRepositoryPath );
+        }
+        projectBuildingRequest =
+            repositoryManager.setLocalRepositoryBasedir( session.getProjectBuildingRequest(), localRepositoryPath );
+    }
+
+    /**
+     * Installs the specified artifact to the local repository. Note: This method should only be used for artifacts that
+     * originate from the current (reactor) build. Artifacts that have been grabbed from the user's local repository
+     * should be installed to the test repository via {@link #copyArtifact(File, Artifact)}.
+     *
+     * @param file The file associated with the artifact, must not be <code>null</code>. This is in most cases the value
+     *            of <code>artifact.getFile()</code> with the exception of the main artifact from a project with
+     *            packaging "pom". Projects with packaging "pom" have no main artifact file. They have however artifact
+     *            metadata (e.g. site descriptors) which needs to be installed.
+     * @param artifact The artifact to install, must not be <code>null</code>.
+     * @throws MojoExecutionException If the artifact could not be installed (e.g. has no associated file).
+     */
+    private void installArtifact( File file, Artifact artifact )
+        throws MojoExecutionException
+    {
+        try
+        {
+            if ( file == null )
+            {
+                throw new IllegalStateException( "Artifact has no associated file: " + artifact.getId() );
+            }
+            if ( !file.isFile() )
+            {
+                throw new IllegalStateException( "Artifact is not fully assembled: " + file );
+            }
+
+            if ( installedArtifacts.add( artifact.getId() ) )
+            {
+                artifact.setFile( file );
+                installer.install( projectBuildingRequest, localRepositoryPath,
+                                   Collections.singletonList( artifact ) );
+            }
+            else
+            {
+                getLog().debug( "Not re-installing " + artifact + ", " + file );
+            }
+        }
+        catch ( Exception e )
+        {
+            throw new MojoExecutionException( "Failed to install artifact: " + artifact, e );
+        }
+    }
+
+    /**
+     * Installs the specified artifact to the local repository. This method serves basically the same purpose as
+     * {@link #installArtifact(File, Artifact)} but is meant for artifacts that have been resolved
+     * from the user's local repository (and not the current build outputs). The subtle difference here is that
+     * artifacts from the repository have already undergone transformations and these manipulations should not be redone
+     * by the artifact installer. For this reason, this method performs plain copy operations to install the artifacts.
+     *
+     * @param file The file associated with the artifact, must not be <code>null</code>.
+     * @param artifact The artifact to install, must not be <code>null</code>.
+     * @throws MojoExecutionException If the artifact could not be installed (e.g. has no associated file).
+     */
+    private void copyArtifact( File file, Artifact artifact )
+        throws MojoExecutionException
+    {
+        try
+        {
+            if ( file == null )
+            {
+                throw new IllegalStateException( "Artifact has no associated file: " + artifact.getId() );
+            }
+            if ( !file.isFile() )
+            {
+                throw new IllegalStateException( "Artifact is not fully assembled: " + file );
+            }
+
+            if ( copiedArtifacts.add( artifact.getId() ) )
+            {
+                File destination =
+                    new File( localRepositoryPath,
+                              repositoryManager.getPathForLocalArtifact( projectBuildingRequest, artifact ) );
+
+                getLog().debug( "Installing " + file + " to " + destination );
+
+                copyFileIfDifferent( file, destination );
+
+                MetadataUtils.createMetadata( destination, artifact );
+            }
+            else
+            {
+                getLog().debug( "Not re-installing " + artifact + ", " + file );
+            }
+        }
+        catch ( Exception e )
+        {
+            throw new MojoExecutionException( "Failed to stage artifact: " + artifact, e );
+        }
+    }
+
+    private void copyFileIfDifferent( File src, File dst )
+        throws IOException
+    {
+        if ( src.lastModified() != dst.lastModified() || src.length() != dst.length() )
+        {
+            FileUtils.copyFile( src, dst );
+            dst.setLastModified( src.lastModified() );
+        }
+    }
+
+    /**
+     * Installs the main artifact and any attached artifacts of the specified project to the local repository.
+     *
+     * @param mvnProject The project whose artifacts should be installed, must not be <code>null</code>.
+     * @throws MojoExecutionException If any artifact could not be installed.
+     */
+    private void installProjectArtifacts( MavenProject mvnProject )
+        throws MojoExecutionException
+    {
+        try
+        {
+            // Install POM (usually attached as metadata but that happens only as a side effect of the Install Plugin)
+            installProjectPom( mvnProject );
+
+            // Install the main project artifact (if the project has one, e.g. has no "pom" packaging)
+            Artifact mainArtifact = mvnProject.getArtifact();
+            if ( mainArtifact.getFile() != null )
+            {
+                installArtifact( mainArtifact.getFile(), mainArtifact );
+            }
+
+            // Install any attached project artifacts
+            Collection<Artifact> attachedArtifacts = (Collection<Artifact>) mvnProject.getAttachedArtifacts();
+            for ( Artifact attachedArtifact : attachedArtifacts )
+            {
+                installArtifact( attachedArtifact.getFile(), attachedArtifact );
+            }
+        }
+        catch ( Exception e )
+        {
+            throw new MojoExecutionException( "Failed to install project artifacts: " + mvnProject, e );
+        }
+    }
+
+    /**
+     * Installs the (locally reachable) parent POMs of the specified project to the local repository. The parent POMs
+     * from the reactor must be installed or the forked IT builds will fail when using a clean repository.
+     *
+     * @param mvnProject The project whose parent POMs should be installed, must not be <code>null</code>.
+     * @throws MojoExecutionException If any POM could not be installed.
+     */
+    private void installProjectParents( MavenProject mvnProject )
+        throws MojoExecutionException
+    {
+        try
+        {
+            for ( MavenProject parent = mvnProject.getParent(); parent != null; parent = parent.getParent() )
+            {
+                if ( parent.getFile() == null )
+                {
+                    copyParentPoms( parent.getGroupId(), parent.getArtifactId(), parent.getVersion() );
+                    break;
+                }
+                installProjectPom( parent );
+            }
+        }
+        catch ( Exception e )
+        {
+            throw new MojoExecutionException( "Failed to install project parents: " + mvnProject, e );
+        }
+    }
+
+    /**
+     * Installs the POM of the specified project to the local repository.
+     *
+     * @param mvnProject The project whose POM should be installed, must not be <code>null</code>.
+     * @throws MojoExecutionException If the POM could not be installed.
+     */
+    private void installProjectPom( MavenProject mvnProject )
+        throws MojoExecutionException
+    {
+        try
+        {
+            Artifact pomArtifact = null;
+            if ( "pom".equals( mvnProject.getPackaging() ) )
+            {
+                pomArtifact = mvnProject.getArtifact();
+            }
+            if ( pomArtifact == null )
+            {
+                pomArtifact =
+                    artifactFactory.createProjectArtifact( mvnProject.getGroupId(), mvnProject.getArtifactId(),
+                                                           mvnProject.getVersion() );
+            }
+            installArtifact( mvnProject.getFile(), pomArtifact );
+        }
+        catch ( Exception e )
+        {
+            throw new MojoExecutionException( "Failed to install POM: " + mvnProject, e );
+        }
+    }
+
+    /**
+     * Installs the dependent projects from the reactor to the local repository. The dependencies on other modules from
+     * the reactor must be installed or the forked IT builds will fail when using a clean repository.
+     *
+     * @param mvnProject The project whose dependent projects should be installed, must not be <code>null</code>.
+     * @param reactorProjects The set of projects in the reactor build, must not be <code>null</code>.
+     * @throws MojoExecutionException If any dependency could not be installed.
+     */
+    private void installProjectDependencies( MavenProject mvnProject, Collection<MavenProject> reactorProjects )
+        throws MojoExecutionException
+    {
+        // keep track if we have passed mvnProject in reactorProjects
+        boolean foundCurrent = false;
+
+        // ... into dependencies that were resolved from reactor projects ...
+        Collection<String> dependencyProjects = new LinkedHashSet<String>();
+
+        // index available reactor projects
+        Map<String, MavenProject> projects = new HashMap<String, MavenProject>( reactorProjects.size() );
+        for ( MavenProject reactorProject : reactorProjects )
+        {
+            String projectId =
+                reactorProject.getGroupId() + ':' + reactorProject.getArtifactId() + ':' + reactorProject.getVersion();
+
+            projects.put( projectId, reactorProject );
+
+            // only add projects of reactor build previous to this mvnProject
+            foundCurrent |= ( mvnProject.equals( reactorProject ) );
+            if ( !foundCurrent )
+            {
+                dependencyProjects.add( projectId );
+            }
+        }
+
+        // group transitive dependencies (even those that don't contribute to the class path like POMs) ...
+        Collection<Artifact> artifacts = (Collection<Artifact>) mvnProject.getArtifacts();
+        // ... and those that were resolved from the (local) repo
+        Collection<Artifact> dependencyArtifacts = new LinkedHashSet<Artifact>();
+
+        for ( Artifact artifact : artifacts )
+        {
+            // workaround for MNG-2961 to ensure the base version does not contain a timestamp
+            artifact.isSnapshot();
+
+            String projectId = artifact.getGroupId() + ':' + artifact.getArtifactId() + ':' + artifact.getBaseVersion();
+
+            if ( !projects.containsKey( projectId ) )
+            {
+                dependencyArtifacts.add( artifact );
+            }
+        }
+
+        // install dependencies
+        try
+        {
+            // copy dependencies that where resolved from the local repo
+            for ( Artifact artifact : dependencyArtifacts )
+            {
+                copyArtifact( artifact );
+            }
+
+            // install dependencies that were resolved from the reactor
+            for ( String projectId : dependencyProjects )
+            {
+                MavenProject dependencyProject = projects.get( projectId );
+
+                installProjectArtifacts( dependencyProject );
+                installProjectParents( dependencyProject );
+            }
+        }
+        catch ( Exception e )
+        {
+            throw new MojoExecutionException( "Failed to install project dependencies: " + mvnProject, e );
+        }
+    }
+
+    private void copyArtifact( Artifact artifact )
+        throws MojoExecutionException
+    {
+        copyPoms( artifact );
+
+        Artifact depArtifact =
+            artifactFactory.createArtifactWithClassifier( artifact.getGroupId(), artifact.getArtifactId(),
+                                                          artifact.getBaseVersion(), artifact.getType(),
+                                                          artifact.getClassifier() );
+
+        File artifactFile = artifact.getFile();
+
+        copyArtifact( artifactFile, depArtifact );
+    }
+
+    private void copyPoms( Artifact artifact )
+        throws MojoExecutionException
+    {
+        Artifact pomArtifact =
+            artifactFactory.createProjectArtifact( artifact.getGroupId(), artifact.getArtifactId(),
+                                                   artifact.getBaseVersion() );
+
+        File pomFile = new File( localRepository.getBasedir(), localRepository.pathOf( pomArtifact ) );
+
+        if ( pomFile.isFile() )
+        {
+            copyArtifact( pomFile, pomArtifact );
+            copyParentPoms( pomFile );
+        }
+    }
+
+    /**
+     * Installs all parent POMs of the specified POM file that are available in the local repository.
+     *
+     * @param pomFile The path to the POM file whose parents should be installed, must not be <code>null</code>.
+     * @throws MojoExecutionException If any (existing) parent POM could not be installed.
+     */
+    private void copyParentPoms( File pomFile )
+        throws MojoExecutionException
+    {
+        Model model = PomUtils.loadPom( pomFile );
+        Parent parent = model.getParent();
+        if ( parent != null )
+        {
+            copyParentPoms( parent.getGroupId(), parent.getArtifactId(), parent.getVersion() );
+        }
+    }
+
+    /**
+     * Installs the specified POM and all its parent POMs to the local repository.
+     *
+     * @param groupId The group id of the POM which should be installed, must not be <code>null</code>.
+     * @param artifactId The artifact id of the POM which should be installed, must not be <code>null</code>.
+     * @param version The version of the POM which should be installed, must not be <code>null</code>.
+     * @throws MojoExecutionException If any (existing) parent POM could not be installed.
+     */
+    private void copyParentPoms( String groupId, String artifactId, String version )
+        throws MojoExecutionException
+    {
+        Artifact pomArtifact = artifactFactory.createProjectArtifact( groupId, artifactId, version );
+
+        if ( installedArtifacts.contains( pomArtifact.getId() ) || copiedArtifacts.contains( pomArtifact.getId() ) )
+        {
+            getLog().debug( "Not re-installing " + pomArtifact );
+            return;
+        }
+
+        File pomFile = new File( localRepository.getBasedir(), localRepository.pathOf( pomArtifact ) );
+        if ( pomFile.isFile() )
+        {
+            copyArtifact( pomFile, pomArtifact );
+            copyParentPoms( pomFile );
+        }
+    }
+
+    private void installExtraArtifacts( String[] extraArtifacts )
+        throws MojoExecutionException
+    {
+        if ( extraArtifacts == null )
+        {
+            return;
+        }
+
+        for ( String extraArtifact : extraArtifacts )
+        {
+            String[] gav = extraArtifact.split( ":" );
+            if ( gav.length < 3 || gav.length > 5 )
+            {
+                throw new MojoExecutionException( "Invalid artifact " + extraArtifact );
+            }
+
+            String groupId = gav[0];
+            String artifactId = gav[1];
+            String version = gav[2];
+
+            String type = "jar";
+            if ( gav.length > 3 )
+            {
+                type = gav[3];
+            }
+
+            String classifier = null;
+            if ( gav.length == 5 )
+            {
+                classifier = gav[4];
+            }
+
+            DefaultDependableCoordinate coordinate = new DefaultDependableCoordinate();
+            try
+            {
+                coordinate.setGroupId( groupId );
+                coordinate.setArtifactId( artifactId );
+                coordinate.setVersion( version );
+                coordinate.setType( type );
+                coordinate.setClassifier( classifier );
+
+                resolver.resolveDependencies( projectBuildingRequest, coordinate, null );
+            }
+            catch ( DependencyResolverException e )
+            {
+                throw new MojoExecutionException( "Unable to resolve dependencies for: " + coordinate, e );
+            }
+        }
+    }
+
+}

Added: maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/IntegrationTestMojo.java
URL: http://svn.apache.org/viewvc/maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/IntegrationTestMojo.java?rev=1795243&view=auto
==============================================================================
--- maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/IntegrationTestMojo.java (added)
+++ maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/IntegrationTestMojo.java Mon May 15 21:10:27 2017
@@ -0,0 +1,48 @@
+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 org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+
+/**
+ * Searches for integration test Maven projects, and executes each, collecting a log in the project directory, will
+ * never fail the build, designed to be used in conjunction with the verify mojo.
+ *
+ * @since 1.4
+ * @author <a href="mailto:stephenconnolly at codehaus">Stephen Connolly</a>
+ * @version $Id: IntegrationTestMojo.java 1637968 2014-11-10 20:02:25Z khmarbaise $
+ */
+// CHECKSTYLE_OFF: LineLength
+@Mojo( name = "integration-test", defaultPhase = LifecyclePhase.INTEGRATION_TEST, requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true )
+public class IntegrationTestMojo
+    extends AbstractInvokerMojo
+{
+
+    void processResults( InvokerSession invokerSession )
+        throws MojoFailureException
+    {
+        // do nothing
+    }
+
+}
+// CHECKSTYLE_ON: LineLength

Added: maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerMojo.java
URL: http://svn.apache.org/viewvc/maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerMojo.java?rev=1795243&view=auto
==============================================================================
--- maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerMojo.java (added)
+++ maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerMojo.java Mon May 15 21:10:27 2017
@@ -0,0 +1,82 @@
+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 org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+
+/**
+ * Searches for integration test Maven projects, and executes each, collecting a log in the project directory, and
+ * outputting the results to the command line.
+ *
+ * @since 1.0
+ * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
+ * @author <a href="mailto:jdcasey@apache.org">John Casey</a>
+ * @version $Id: InvokerMojo.java 1637968 2014-11-10 20:02:25Z khmarbaise $
+ */
+// CHECKSTYLE_OFF: LineLength
+@Mojo( name = "run", defaultPhase = LifecyclePhase.INTEGRATION_TEST, requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true )
+// CHECKSTYLE_ON: LineLength
+public class InvokerMojo
+    extends AbstractInvokerMojo
+{
+
+    /**
+     * A flag controlling whether failures of the sub builds should fail the main build, too. If set to
+     * <code>true</code>, the main build will proceed even if one or more sub builds failed.
+     *
+     * @since 1.3
+     */
+    @Parameter( property = "maven.test.failure.ignore", defaultValue = "false" )
+    private boolean ignoreFailures;
+
+    /**
+     * Set this to <code>true</code> to cause a failure if there are no projects to invoke.
+     *
+     * @since 1.9
+     */
+    @Parameter( property = "invoker.failIfNoProjects" )
+    private Boolean failIfNoProjects;
+
+    void processResults( InvokerSession invokerSession )
+        throws MojoFailureException
+    {
+        if ( !suppressSummaries )
+        {
+            invokerSession.logSummary( getLog(), ignoreFailures );
+        }
+
+        invokerSession.handleFailures( getLog(), ignoreFailures );
+    }
+
+    @Override
+    protected void doFailIfNoProjects()
+        throws MojoFailureException
+    {
+        if ( Boolean.TRUE.equals( failIfNoProjects ) )
+        {
+            throw new MojoFailureException( "No projects to invoke!" );
+        }
+    }
+
+}

Added: maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerProperties.java
URL: http://svn.apache.org/viewvc/maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerProperties.java?rev=1795243&view=auto
==============================================================================
--- maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerProperties.java (added)
+++ maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerProperties.java Mon May 15 21:10:27 2017
@@ -0,0 +1,365 @@
+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.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Properties;
+
+import org.apache.maven.shared.invoker.InvocationRequest;
+import org.apache.maven.shared.invoker.InvocationRequest.ReactorFailureBehavior;
+import org.codehaus.plexus.util.StringUtils;
+
+/**
+ * Provides a convenient facade around the <code>invoker.properties</code>.
+ *
+ * @author Benjamin Bentmann
+ * @version $Id: InvokerProperties.java 1779250 2017-01-17 20:20:02Z rfscholte $
+ */
+class InvokerProperties
+{
+    private static final String SELECTOR_PREFIX = "selector.";
+
+    private enum InvocationProperty
+    {
+        PROJECT( "invoker.project" ),
+        GOALS( "invoker.goals" ),
+        PROFILES( "invoker.profiles" ),
+        MAVEN_OPTS( "invoker.mavenOpts" ),
+        FAILURE_BEHAVIOR( "invoker.failureBehavior" ),
+        NON_RECURSIVE( "invoker.nonRecursive" ),
+        OFFLINE( "invoker.offline" ),
+        SYSTEM_PROPERTIES_FILE( "invoker.systemPropertiesFile" ),
+        DEBUG( "invoker.debug" );
+
+        private final String key;
+
+        private InvocationProperty( final String s )
+        {
+            this.key = s;
+        }
+
+        @Override
+        public String toString()
+        {
+            return key;
+        }
+    }
+    
+    private enum SelectorProperty
+    {
+        JAVA_VERSION( ".java.version" ),
+        MAVEN_VERSION( ".maven.version" ),
+        OS_FAMLY( ".os.family" );
+        
+        private final String suffix;
+        
+        private SelectorProperty( String suffix )
+        {
+            this.suffix = suffix;
+        }
+        
+        @Override
+        public String toString()
+        {
+            return suffix;
+        }
+    }
+
+    /**
+     * The invoker properties being wrapped.
+     */
+    private final Properties properties;
+
+    /**
+     * Creates a new facade for the specified invoker properties. The properties will not be copied, so any changes to
+     * them will be reflected by the facade.
+     *
+     * @param properties The invoker properties to wrap, may be <code>null</code> if none.
+     */
+    public InvokerProperties( Properties properties )
+    {
+        this.properties = ( properties != null ) ? properties : new Properties();
+    }
+
+    /**
+     * Gets the invoker properties being wrapped.
+     *
+     * @return The invoker properties being wrapped, never <code>null</code>.
+     */
+    public Properties getProperties()
+    {
+        return this.properties;
+    }
+
+    /**
+     * Gets the name of the corresponding build job.
+     *
+     * @return The name of the build job or an empty string if not set.
+     */
+    public String getJobName()
+    {
+        return this.properties.getProperty( "invoker.name", "" );
+    }
+
+    /**
+     * Gets the description of the corresponding build job.
+     *
+     * @return The description of the build job or an empty string if not set.
+     */
+    public String getJobDescription()
+    {
+        return this.properties.getProperty( "invoker.description", "" );
+    }
+
+    /**
+     * Gets the specification of JRE versions on which this build job should be run.
+     *
+     * @return The specification of JRE versions or an empty string if not set.
+     */
+    public String getJreVersion()
+    {
+        return this.properties.getProperty( "invoker.java.version", "" );
+    }
+
+    /**
+     * Gets the specification of JRE versions on which this build job should be run.
+     *
+     * @return The specification of JRE versions or an empty string if not set.
+     */
+    public String getJreVersion( int index )
+    {
+        return this.properties.getProperty( SELECTOR_PREFIX + index + SelectorProperty.JAVA_VERSION.suffix,
+                                            getJreVersion() );
+    }
+
+    /**
+     * Gets the specification of Maven versions on which this build job should be run.
+     *
+     * @return The specification of Maven versions on which this build job should be run.
+     * @since 1.5
+     */
+    public String getMavenVersion()
+    {
+        return this.properties.getProperty( "invoker.maven.version", "" );
+    }
+    
+    /**
+     * 
+     * @param index the selector index
+     * @return The specification of Maven versions on which this build job should be run.
+     * @since 3.0.0
+     */
+    public String getMavenVersion( int index )
+    {
+        return this.properties.getProperty( SELECTOR_PREFIX + index + SelectorProperty.MAVEN_VERSION.suffix,
+                                            getMavenVersion() );
+    }
+
+    /**
+     * Gets the specification of OS families on which this build job should be run.
+     *
+     * @return The specification of OS families or an empty string if not set.
+     */
+    public String getOsFamily()
+    {
+        return this.properties.getProperty( "invoker.os.family", "" );
+    }
+    
+    /**
+     * Gets the specification of OS families on which this build job should be run.
+     *
+     * @param index the selector index
+     * @return The specification of OS families or an empty string if not set.
+     * @since 3.0.0
+     */
+    public String getOsFamily( int index )
+    {
+        return this.properties.getProperty( SELECTOR_PREFIX + index + SelectorProperty.OS_FAMLY.suffix,
+                                            getOsFamily() );
+    }   
+    
+
+    /**
+     * Determines whether these invoker properties contain a build definition for the specified invocation index.
+     *
+     * @param index The one-based index of the invocation to check for, must not be negative.
+     * @return <code>true</code> if the invocation with the specified index is defined, <code>false</code> otherwise.
+     */
+    public boolean isInvocationDefined( int index )
+    {
+        for ( InvocationProperty prop : InvocationProperty.values() )
+        {
+            if ( properties.getProperty( prop.toString() + '.' + index ) != null )
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Determines whether these invoker properties contain a build definition for the specified selector index.
+     * 
+     * @param index the index
+     * @return <code>true</code> if the selector with the specified index is defined, <code>false</code> otherwise.
+     * @since 3.0.0
+     */
+    public boolean isSelectorDefined( int index )
+    {
+        for ( SelectorProperty prop : SelectorProperty.values() )
+        {
+            if ( properties.getProperty( SELECTOR_PREFIX + index + prop.suffix ) != null )
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Configures the specified invocation request from these invoker properties. Settings not present in the invoker
+     * properties will be left unchanged in the invocation request.
+     *
+     * @param request The invocation request to configure, must not be <code>null</code>.
+     * @param index The one-based index of the invocation to configure, must not be negative.
+     */
+    public void configureInvocation( InvocationRequest request, int index )
+    {
+        String project = get( InvocationProperty.PROJECT, index );
+        if ( project != null )
+        {
+            File file = new File( request.getBaseDirectory(), project );
+            if ( file.isFile() )
+            {
+                request.setBaseDirectory( file.getParentFile() );
+                request.setPomFile( file );
+            }
+            else
+            {
+                request.setBaseDirectory( file );
+                request.setPomFile( null );
+            }
+        }
+
+        String goals = get( InvocationProperty.GOALS, index );
+        if ( goals != null )
+        {
+            request.setGoals( new ArrayList<String>( Arrays.asList( StringUtils.split( goals, ", \t\n\r\f" ) ) ) );
+        }
+
+        String profiles = get( InvocationProperty.PROFILES, index );
+        if ( profiles != null )
+        {
+            // CHECKSTYLE_OFF: LineLength
+            request.setProfiles( new ArrayList<String>( Arrays.asList( StringUtils.split( profiles,
+                                                                                          ", \t\n\r\f" ) ) ) );
+            // CHECKSTYLE_ON: LineLength
+        }
+
+        String mvnOpts = get( InvocationProperty.MAVEN_OPTS, index );
+        if ( mvnOpts != null )
+        {
+            request.setMavenOpts( mvnOpts );
+        }
+
+        String failureBehavior = get( InvocationProperty.FAILURE_BEHAVIOR, index );
+        if ( failureBehavior != null )
+        {
+            ReactorFailureBehavior valueOf =
+                InvocationRequest.ReactorFailureBehavior.valueOfByLongOption( failureBehavior );
+            request.setReactorFailureBehavior( valueOf );
+        }
+
+        String nonRecursive = get( InvocationProperty.NON_RECURSIVE, index );
+        if ( nonRecursive != null )
+        {
+            request.setRecursive( !Boolean.valueOf( nonRecursive ) );
+        }
+
+        String offline = get( InvocationProperty.OFFLINE, index );
+        if ( offline != null )
+        {
+            request.setOffline( Boolean.valueOf( offline ) );
+        }
+
+        String debug = get( InvocationProperty.DEBUG, index );
+        if ( debug != null )
+        {
+            request.setDebug( Boolean.valueOf( debug ) );
+        }
+    }
+
+    /**
+     * Checks whether the specified exit code matches the one expected for the given invocation.
+     *
+     * @param exitCode The exit code of the Maven invocation to check.
+     * @param index The index of the invocation for which to check the exit code, must not be negative.
+     * @return <code>true</code> if the exit code is zero and a success was expected or if the exit code is non-zero and
+     *         a failue was expected, <code>false</code> otherwise.
+     */
+    public boolean isExpectedResult( int exitCode, int index )
+    {
+        boolean nonZeroExit = "failure".equalsIgnoreCase( get( "invoker.buildResult", index ) );
+        return ( exitCode != 0 ) == nonZeroExit;
+    }
+
+    /**
+     * Gets the path to the properties file used to set the system properties for the specified invocation.
+     *
+     * @param index The index of the invocation for which to check the exit code, must not be negative.
+     * @return The path to the properties file or <code>null</code> if not set.
+     */
+    public String getSystemPropertiesFile( int index )
+    {
+        return get( InvocationProperty.SYSTEM_PROPERTIES_FILE, index );
+    }
+
+    /**
+     * Gets a value from the invoker properties. The invoker properties are intended to describe the invocation settings
+     * for multiple builds of the same project. For this reason, the properties are indexed. First, a property named
+     * <code>key.index</code> will be queried. If this property does not exist, the value of the property named
+     * <code>key</code> will finally be returned.
+     *
+     * @param key The (base) key for the invoker property to lookup, must not be <code>null</code>.
+     * @param index The index of the invocation for which to retrieve the value, must not be negative.
+     * @return The value for the requested invoker property or <code>null</code> if not defined.
+     */
+    String get( String key, int index )
+    {
+        if ( index < 0 )
+        {
+            throw new IllegalArgumentException( "invalid invocation index: " + index );
+        }
+
+        String value = properties.getProperty( key + '.' + index );
+        if ( value == null )
+        {
+            value = properties.getProperty( key );
+        }
+        return value;
+    }
+
+    private String get( InvocationProperty prop, int index )
+    {
+        return get( prop.toString(), index );
+    }
+}

Added: maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerReport.java
URL: http://svn.apache.org/viewvc/maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerReport.java?rev=1795243&view=auto
==============================================================================
--- maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerReport.java (added)
+++ maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerReport.java Mon May 15 21:10:27 2017
@@ -0,0 +1,357 @@
+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.io.File;
+import java.io.IOException;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.maven.doxia.sink.Sink;
+import org.apache.maven.doxia.siterenderer.Renderer;
+import org.apache.maven.plugins.invoker.model.BuildJob;
+import org.apache.maven.plugins.invoker.model.io.xpp3.BuildJobXpp3Reader;
+import org.apache.maven.plugins.annotations.Component;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.reporting.AbstractMavenReport;
+import org.apache.maven.reporting.MavenReportException;
+import org.codehaus.plexus.i18n.I18N;
+import org.codehaus.plexus.util.ReaderFactory;
+import org.codehaus.plexus.util.StringUtils;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
+
+/**
+ * Generate a report based on the results of the Maven invocations. <strong>Note:</strong> This mojo doesn't fork any
+ * lifecycle, if you have a clean working copy, you have to use a command like
+ * <code>mvn clean integration-test site</code> to ensure the build results are present when this goal is invoked.
+ *
+ * @author Olivier Lamy
+ * @since 1.4
+ */
+@Mojo( name = "report", threadSafe = true )
+public class InvokerReport
+    extends AbstractMavenReport
+{
+
+    /**
+     * The Maven Project.
+     */
+    @Parameter( defaultValue = "${project}", readonly = true, required = true )
+    protected MavenProject project;
+
+    /**
+     * Doxia Site Renderer component.
+     */
+    @Component
+    protected Renderer siteRenderer;
+
+    /**
+     * Internationalization component.
+     */
+    @Component
+    protected I18N i18n;
+
+    /**
+     * The output directory for the report. Note that this parameter is only evaluated if the goal is run directly from
+     * the command line. If the goal is run indirectly as part of a site generation, the output directory configured in
+     * the Maven Site Plugin is used instead.
+     */
+    @Parameter( defaultValue = "${project.reporting.outputDirectory}", required = true )
+    protected File outputDirectory;
+
+    /**
+     * Base directory where all build reports have been written to.
+     */
+    @Parameter( defaultValue = "${project.build.directory}/invoker-reports", property = "invoker.reportsDirectory" )
+    private File reportsDirectory;
+
+    /**
+     * The number format used to print percent values in the report locale.
+     */
+    private NumberFormat percentFormat;
+
+    /**
+     * The number format used to print time values in the report locale.
+     */
+    private NumberFormat secondsFormat;
+
+    protected void executeReport( Locale locale )
+        throws MavenReportException
+    {
+        DecimalFormatSymbols symbols = new DecimalFormatSymbols( locale );
+        percentFormat = new DecimalFormat( getText( locale, "report.invoker.format.percent" ), symbols );
+        secondsFormat = new DecimalFormat( getText( locale, "report.invoker.format.seconds" ), symbols );
+
+        Sink sink = getSink();
+
+        sink.head();
+
+        sink.title();
+        sink.text( getText( locale, "report.invoker.result.title" ) );
+        sink.title_();
+
+        sink.head_();
+
+        sink.body();
+
+        sink.section1();
+        sink.sectionTitle1();
+        sink.text( getText( locale, "report.invoker.result.title" ) );
+        sink.sectionTitle1_();
+        sink.paragraph();
+        sink.text( getText( locale, "report.invoker.result.description" ) );
+        sink.paragraph_();
+        sink.section1_();
+
+        // ----------------------------------
+        // build buildJob beans
+        // ----------------------------------
+        File[] reportFiles = ReportUtils.getReportFiles( reportsDirectory );
+        if ( reportFiles.length <= 0 )
+        {
+            getLog().info( "no invoker report files found, skip report generation" );
+            return;
+        }
+
+        List<BuildJob> buildJobs = new ArrayList<BuildJob>( reportFiles.length );
+        for ( File reportFile : reportFiles )
+        {
+            try
+            {
+                BuildJobXpp3Reader reader = new BuildJobXpp3Reader();
+                buildJobs.add( reader.read( ReaderFactory.newXmlReader( reportFile ) ) );
+            }
+            catch ( XmlPullParserException e )
+            {
+                throw new MavenReportException( "Failed to parse report file: " + reportFile, e );
+            }
+            catch ( IOException e )
+            {
+                throw new MavenReportException( "Failed to read report file: " + reportFile, e );
+            }
+        }
+
+        // ----------------------------------
+        // summary
+        // ----------------------------------
+
+        constructSummarySection( buildJobs, locale );
+
+        // ----------------------------------
+        // per file/it detail
+        // ----------------------------------
+
+        sink.section2();
+        sink.sectionTitle2();
+
+        sink.text( getText( locale, "report.invoker.detail.title" ) );
+
+        sink.sectionTitle2_();
+
+        sink.section2_();
+
+        // detail tests table header
+        sink.table();
+
+        sink.tableRow();
+        // -------------------------------------------
+        // name | Result | time | message
+        // -------------------------------------------
+        sinkTableHeader( sink, getText( locale, "report.invoker.detail.name" ) );
+        sinkTableHeader( sink, getText( locale, "report.invoker.detail.result" ) );
+        sinkTableHeader( sink, getText( locale, "report.invoker.detail.time" ) );
+        sinkTableHeader( sink, getText( locale, "report.invoker.detail.message" ) );
+
+        sink.tableRow_();
+
+        for ( BuildJob buildJob : buildJobs )
+        {
+            renderBuildJob( buildJob, locale );
+        }
+
+        sink.table_();
+
+        sink.body_();
+
+        sink.flush();
+        sink.close();
+    }
+
+    private void constructSummarySection( List<? extends BuildJob> buildJobs, Locale locale )
+    {
+        Sink sink = getSink();
+
+        sink.section2();
+        sink.sectionTitle2();
+
+        sink.text( getText( locale, "report.invoker.summary.title" ) );
+
+        sink.sectionTitle2_();
+        sink.section2_();
+
+        // ------------------------------------------------------------------------
+        // Building a table with
+        // it number | succes nb | failed nb | Success rate | total time | avg time
+        // ------------------------------------------------------------------------
+
+        sink.table();
+        sink.tableRow();
+
+        sinkTableHeader( sink, getText( locale, "report.invoker.summary.number" ) );
+        sinkTableHeader( sink, getText( locale, "report.invoker.summary.success" ) );
+        sinkTableHeader( sink, getText( locale, "report.invoker.summary.failed" ) );
+        sinkTableHeader( sink, getText( locale, "report.invoker.summary.skipped" ) );
+        sinkTableHeader( sink, getText( locale, "report.invoker.summary.success.rate" ) );
+        sinkTableHeader( sink, getText( locale, "report.invoker.summary.time.total" ) );
+        sinkTableHeader( sink, getText( locale, "report.invoker.summary.time.avg" ) );
+
+        int number = buildJobs.size();
+        int success = 0;
+        int failed = 0;
+        int skipped = 0;
+        double totalTime = 0;
+
+        for ( BuildJob buildJob : buildJobs )
+        {
+            if ( BuildJob.Result.SUCCESS.equals( buildJob.getResult() ) )
+            {
+                success++;
+            }
+            else if ( BuildJob.Result.SKIPPED.equals( buildJob.getResult() ) )
+            {
+                skipped++;
+            }
+            else
+            {
+                failed++;
+            }
+            totalTime += buildJob.getTime();
+        }
+
+        sink.tableRow_();
+        sink.tableRow();
+
+        sinkCell( sink, Integer.toString( number ) );
+        sinkCell( sink, Integer.toString( success ) );
+        sinkCell( sink, Integer.toString( failed ) );
+        sinkCell( sink, Integer.toString( skipped ) );
+
+        if ( success + failed > 0 )
+        {
+            sinkCell( sink, percentFormat.format( (double) success / ( success + failed ) ) );
+        }
+        else
+        {
+            sinkCell( sink, "" );
+        }
+
+        sinkCell( sink, secondsFormat.format( totalTime ) );
+
+        sinkCell( sink, secondsFormat.format( totalTime / number ) );
+
+        sink.tableRow_();
+        sink.table_();
+
+    }
+
+    private void renderBuildJob( BuildJob buildJob, Locale locale )
+    {
+        Sink sink = getSink();
+        sink.tableRow();
+        StringBuilder buffer = new StringBuilder();
+        if ( !StringUtils.isEmpty( buildJob.getName() ) && !StringUtils.isEmpty( buildJob.getDescription() ) )
+        {
+            buffer.append( buildJob.getName() );
+            buffer.append( " : " );
+            buffer.append( buildJob.getDescription() );
+        }
+        else
+        {
+            buffer.append( buildJob.getProject() );
+        }
+        sinkCell( sink, buffer.toString() );
+        // FIXME image
+        sinkCell( sink, buildJob.getResult() );
+        sinkCell( sink, secondsFormat.format( buildJob.getTime() ) );
+        sinkCell( sink, buildJob.getFailureMessage() );
+        sink.tableRow_();
+    }
+
+    protected String getOutputDirectory()
+    {
+        return outputDirectory.getAbsolutePath();
+    }
+
+    protected MavenProject getProject()
+    {
+        return project;
+    }
+
+    protected Renderer getSiteRenderer()
+    {
+        return siteRenderer;
+    }
+
+    public String getDescription( Locale locale )
+    {
+        return getText( locale, "report.invoker.result.description" );
+    }
+
+    public String getName( Locale locale )
+    {
+        return getText( locale, "report.invoker.result.name" );
+    }
+
+    public String getOutputName()
+    {
+        return "invoker-report";
+    }
+
+    public boolean canGenerateReport()
+    {
+        return ReportUtils.getReportFiles( reportsDirectory ).length > 0;
+    }
+
+    private String getText( Locale locale, String key )
+    {
+        return i18n.getString( "invoker-report", locale, key );
+    }
+
+    private void sinkTableHeader( Sink sink, String header )
+    {
+        sink.tableHeaderCell();
+        sink.text( header );
+        sink.tableHeaderCell_();
+    }
+
+    private void sinkCell( Sink sink, String text )
+    {
+        sink.tableCell();
+        sink.text( text );
+        sink.tableCell_();
+    }
+
+}

Added: maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerSession.java
URL: http://svn.apache.org/viewvc/maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerSession.java?rev=1795243&view=auto
==============================================================================
--- maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerSession.java (added)
+++ maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/InvokerSession.java Mon May 15 21:10:27 2017
@@ -0,0 +1,279 @@
+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 static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.invoker.model.BuildJob;
+import org.apache.maven.plugin.logging.Log;
+
+/**
+ * Tracks a set of build jobs and their results.
+ *
+ * @author Benjamin Bentmann
+ */
+class InvokerSession
+{
+
+    private List<BuildJob> buildJobs;
+
+    private List<BuildJob> failedJobs;
+
+    private List<BuildJob> errorJobs;
+
+    private List<BuildJob> successfulJobs;
+
+    private List<BuildJob> skippedJobs;
+
+    /**
+     * Creates a new empty session.
+     */
+    public InvokerSession()
+    {
+        buildJobs = new ArrayList<BuildJob>();
+    }
+
+    /**
+     * Creates a session that initially contains the specified build jobs.
+     *
+     * @param buildJobs The build jobs to set, must not be <code>null</code>.
+     */
+    public InvokerSession( BuildJob[] buildJobs )
+    {
+        this.buildJobs = new ArrayList<BuildJob>( Arrays.asList( buildJobs ) );
+    }
+
+    /**
+     * Adds the specified build job to this session.
+     *
+     * @param buildJob The build job to add, must not be <code>null</code>.
+     */
+    public void addJob( BuildJob buildJob )
+    {
+        buildJobs.add( buildJob );
+
+        resetStats();
+    }
+
+    /**
+     * Sets the build jobs of this session.
+     *
+     * @param buildJobs The build jobs to set, must not be <code>null</code>.
+     */
+    public void setJobs( List<? extends BuildJob> buildJobs )
+    {
+        this.buildJobs = new ArrayList<BuildJob>( buildJobs );
+
+        resetStats();
+    }
+
+    /**
+     * Gets the build jobs in this session.
+     *
+     * @return The build jobs in this session, can be empty but never <code>null</code>.
+     */
+    public List<BuildJob> getJobs()
+    {
+        return buildJobs;
+    }
+
+    /**
+     * Gets the successful build jobs in this session.
+     *
+     * @return The successful build jobs in this session, can be empty but never <code>null</code>.
+     */
+    public List<BuildJob> getSuccessfulJobs()
+    {
+        updateStats();
+
+        return successfulJobs;
+    }
+
+    /**
+     * Gets the failed build jobs in this session.
+     *
+     * @return The failed build jobs in this session, can be empty but never <code>null</code>.
+     */
+    public List<BuildJob> getFailedJobs()
+    {
+        updateStats();
+
+        return failedJobs;
+    }
+
+    /**
+     * Gets the build jobs which had errors for this session.
+     *
+     * @return The build jobs in error for this session, can be empty but never <code>null</code>.
+     */
+    public List<BuildJob> getErrorJobs()
+    {
+        updateStats();
+
+        return errorJobs;
+    }
+
+    /**
+     * Gets the skipped build jobs in this session.
+     *
+     * @return The skipped build jobs in this session, can be empty but never <code>null</code>.
+     */
+    public List<BuildJob> getSkippedJobs()
+    {
+        updateStats();
+
+        return skippedJobs;
+    }
+
+    private void resetStats()
+    {
+        successfulJobs = null;
+        failedJobs = null;
+        skippedJobs = null;
+        errorJobs = null;
+    }
+
+    private void updateStats()
+    {
+        if ( successfulJobs != null && skippedJobs != null && failedJobs != null && errorJobs != null )
+        {
+            return;
+        }
+
+        successfulJobs = new ArrayList<BuildJob>();
+        failedJobs = new ArrayList<BuildJob>();
+        skippedJobs = new ArrayList<BuildJob>();
+        errorJobs = new ArrayList<BuildJob>();
+
+        for ( BuildJob buildJob : buildJobs )
+        {
+            if ( BuildJob.Result.SUCCESS.equals( buildJob.getResult() ) )
+            {
+                successfulJobs.add( buildJob );
+            }
+            else if ( BuildJob.Result.SKIPPED.equals( buildJob.getResult() ) )
+            {
+                skippedJobs.add( buildJob );
+            }
+            else if ( BuildJob.Result.ERROR.equals( buildJob.getResult() ) )
+            {
+                errorJobs.add( buildJob );
+            }
+            else if ( buildJob.getResult() != null )
+            {
+                failedJobs.add( buildJob );
+            }
+        }
+    }
+
+    /**
+     * Prints a summary of this session to the specified logger.
+     *
+     * @param logger The mojo logger to output messages to, must not be <code>null</code>.
+     * @param ignoreFailures A flag whether failures should be ignored or whether a build failure should be signaled.
+     */
+    public void logSummary( Log logger, boolean ignoreFailures )
+    {
+        updateStats();
+
+        String separator = buffer().strong( "-------------------------------------------------" ).toString();
+
+        logger.info( separator );
+        logger.info( "Build Summary:" );
+        logger.info( "  Passed: " + successfulJobs.size() + ", Failed: " + failedJobs.size() + ", Errors: "
+            + errorJobs.size() + ", Skipped: " + skippedJobs.size() );
+        logger.info( separator );
+
+        if ( !failedJobs.isEmpty() )
+        {
+            String heading = "The following builds failed:";
+            if ( ignoreFailures )
+            {
+                logger.warn( heading );
+            }
+            else
+            {
+                logger.error( heading );
+            }
+
+            for ( BuildJob buildJob : failedJobs )
+            {
+                String item = "*  " + buildJob.getProject();
+                if ( ignoreFailures )
+                {
+                    logger.warn( item );
+                }
+                else
+                {
+                    logger.error( item );
+                }
+            }
+
+            logger.info( separator );
+        }
+    }
+
+    /**
+     * Handles the build failures in this session.
+     *
+     * @param logger The mojo logger to output messages to, must not be <code>null</code>.
+     * @param ignoreFailures A flag whether failures should be ignored or whether a build failure should be signaled.
+     * @throws MojoFailureException If failures are present and not ignored.
+     */
+    public void handleFailures( Log logger, boolean ignoreFailures )
+        throws MojoFailureException
+    {
+        updateStats();
+
+        if ( !failedJobs.isEmpty() )
+        {
+            String message = failedJobs.size() + " build" + ( failedJobs.size() == 1 ? "" : "s" ) + " failed.";
+
+            if ( ignoreFailures )
+            {
+                logger.warn( "Ignoring that " + message );
+            }
+            else
+            {
+                throw new MojoFailureException( message + " See console output above for details." );
+            }
+        }
+
+        if ( !errorJobs.isEmpty() )
+        {
+            String message = errorJobs.size() + " build" + ( errorJobs.size() == 1 ? "" : "s" ) + " in error.";
+
+            if ( ignoreFailures )
+            {
+                logger.warn( "Ignoring that " + message );
+            }
+            else
+            {
+                throw new MojoFailureException( message + " See console output above for details." );
+            }
+        }
+    }
+
+}

Added: maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/MetadataUtils.java
URL: http://svn.apache.org/viewvc/maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/MetadataUtils.java?rev=1795243&view=auto
==============================================================================
--- maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/MetadataUtils.java (added)
+++ maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/MetadataUtils.java Mon May 15 21:10:27 2017
@@ -0,0 +1,186 @@
+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.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.TimeZone;
+
+import org.apache.maven.artifact.Artifact;
+import org.codehaus.plexus.util.IOUtil;
+import org.codehaus.plexus.util.ReaderFactory;
+import org.codehaus.plexus.util.WriterFactory;
+import org.codehaus.plexus.util.xml.Xpp3Dom;
+import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
+import org.codehaus.plexus.util.xml.Xpp3DomUtils;
+import org.codehaus.plexus.util.xml.Xpp3DomWriter;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
+
+/**
+ * Provides utility methods for artifact metadata processing.
+ *
+ * @author Benjamin Bentmann
+ */
+class MetadataUtils
+{
+
+    /**
+     * Creates local metadata files for the specified artifact. The goal is to simulate the installation of the artifact
+     * by a local build, thereby decoupling the forked builds from the inderministic collection of remote repositories
+     * that are available to the main build and from which the artifact was originally resolved.
+     *
+     * @param file The artifact's file in the local test repository, must not be <code>null</code>.
+     * @param artifact The artifact to create metadata for, must not be <code>null</code>.
+     * @throws IOException If the metadata could not be created.
+     */
+    public static void createMetadata( File file, Artifact artifact )
+        throws IOException
+    {
+        TimeZone tz = java.util.TimeZone.getTimeZone( "UTC" );
+        SimpleDateFormat fmt = new SimpleDateFormat( "yyyyMMddHHmmss" );
+        fmt.setTimeZone( tz );
+        String timestamp = fmt.format( new Date() );
+
+        if ( artifact.isSnapshot() )
+        {
+            File metadataFile = new File( file.getParentFile(), "maven-metadata-local.xml" );
+
+            Xpp3Dom metadata = new Xpp3Dom( "metadata" );
+            addChild( metadata, "groupId", artifact.getGroupId() );
+            addChild( metadata, "artifactId", artifact.getArtifactId() );
+            addChild( metadata, "version", artifact.getBaseVersion() );
+            Xpp3Dom versioning = new Xpp3Dom( "versioning" );
+            versioning.addChild( addChild( new Xpp3Dom( "snapshot" ), "localCopy", "true" ) );
+            addChild( versioning, "lastUpdated", timestamp );
+            metadata.addChild( versioning );
+
+            writeMetadata( metadataFile, metadata );
+        }
+
+        File metadataFile = new File( file.getParentFile().getParentFile(), "maven-metadata-local.xml" );
+
+        Set<String> allVersions = new LinkedHashSet<String>();
+
+        Xpp3Dom metadata = readMetadata( metadataFile );
+
+        if ( metadata != null )
+        {
+            Xpp3Dom versioning = metadata.getChild( "versioning" );
+            if ( versioning != null )
+            {
+                Xpp3Dom versions = versioning.getChild( "versions" );
+                if ( versions != null )
+                {
+
+                    Xpp3Dom[] children = versions.getChildren( "version" );
+                    for ( Xpp3Dom aChildren : children )
+                    {
+                        allVersions.add( aChildren.getValue() );
+                    }
+                }
+            }
+        }
+
+        allVersions.add( artifact.getBaseVersion() );
+
+        metadata = new Xpp3Dom( "metadata" );
+        addChild( metadata, "groupId", artifact.getGroupId() );
+        addChild( metadata, "artifactId", artifact.getArtifactId() );
+        Xpp3Dom versioning = new Xpp3Dom( "versioning" );
+        versioning.addChild( addChildren( new Xpp3Dom( "versions" ), "version", allVersions ) );
+        addChild( versioning, "lastUpdated", timestamp );
+        metadata.addChild( versioning );
+
+        metadata = Xpp3DomUtils.mergeXpp3Dom( metadata, readMetadata( metadataFile ) );
+
+        writeMetadata( metadataFile, metadata );
+    }
+
+    private static Xpp3Dom addChild( Xpp3Dom parent, String childName, String childValue )
+    {
+        Xpp3Dom child = new Xpp3Dom( childName );
+        child.setValue( childValue );
+        parent.addChild( child );
+        return parent;
+    }
+
+    private static Xpp3Dom addChildren( Xpp3Dom parent, String childName, Collection<String> childValues )
+    {
+        for ( String childValue : childValues )
+        {
+            addChild( parent, childName, childValue );
+        }
+        return parent;
+    }
+
+    private static Xpp3Dom readMetadata( File metadataFile )
+        throws IOException
+    {
+        if ( !metadataFile.isFile() )
+        {
+            return null;
+        }
+
+        Reader reader = null;
+        try
+        {
+            reader = ReaderFactory.newXmlReader( metadataFile );
+            final Xpp3Dom xpp3Dom = Xpp3DomBuilder.build( reader );
+            reader.close();
+            reader = null;
+            return xpp3Dom;
+        }
+        catch ( XmlPullParserException e )
+        {
+            throw (IOException) new IOException( e.getMessage() ).initCause( e );
+        }
+        finally
+        {
+            IOUtil.close( reader );
+        }
+    }
+
+    private static void writeMetadata( File metadataFile, Xpp3Dom metadata )
+        throws IOException
+    {
+        metadataFile.getParentFile().mkdirs();
+
+        Writer writer = null;
+        try
+        {
+            writer = WriterFactory.newXmlWriter( metadataFile );
+            Xpp3DomWriter.write( writer, metadata );
+            writer.close();
+            writer = null;
+        }
+        finally
+        {
+            IOUtil.close( writer );
+        }
+    }
+
+}

Added: maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/PomUtils.java
URL: http://svn.apache.org/viewvc/maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/PomUtils.java?rev=1795243&view=auto
==============================================================================
--- maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/PomUtils.java (added)
+++ maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/PomUtils.java Mon May 15 21:10:27 2017
@@ -0,0 +1,74 @@
+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.io.File;
+import java.io.IOException;
+import java.io.Reader;
+
+import org.apache.maven.model.Model;
+import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.codehaus.plexus.util.IOUtil;
+import org.codehaus.plexus.util.ReaderFactory;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
+
+/**
+ * Provides utility methods for POM processing.
+ *
+ * @author Benjamin Bentmann
+ */
+class PomUtils
+{
+
+    /**
+     * Loads the (raw) model from the specified POM file.
+     *
+     * @param pomFile The path to the POM file to load, must not be <code>null</code>.
+     * @return The raw model, never <code>null</code>.
+     * @throws MojoExecutionException If the POM file could not be loaded.
+     */
+    public static Model loadPom( File pomFile )
+        throws MojoExecutionException
+    {
+        Reader reader = null;
+        try
+        {
+            reader = ReaderFactory.newXmlReader( pomFile );
+            final Model model = new MavenXpp3Reader().read( reader, false );
+            reader.close();
+            reader = null;
+            return model;
+        }
+        catch ( XmlPullParserException e )
+        {
+            throw new MojoExecutionException( "Failed to parse POM: " + pomFile, e );
+        }
+        catch ( IOException e )
+        {
+            throw new MojoExecutionException( "Failed to read POM: " + pomFile, e );
+        }
+        finally
+        {
+            IOUtil.close( reader );
+        }
+    }
+
+}