[MINSTALL-115] Install At End feature (no extension) (#15)

This PR makes installAtEnd work as expected even
if maven-install-plugin is not used as extension.

How it works: it uses mojo Contexts to store "state markers":
* presence of marker means project was "processed"
* value of state marker tells what should be done

UTs adjusted to provide plugin context (was null before).
Plugin updated (as required by maven-plugin-testing-harness) and 
cleaned up.
diff --git a/src/it/install-at-end-fail/verify.groovy b/src/it/install-at-end-fail/verify.groovy
index f35990b..a2801f5 100644
--- a/src/it/install-at-end-fail/verify.groovy
+++ b/src/it/install-at-end-fail/verify.groovy
@@ -22,5 +22,5 @@
 
 File buildLog = new File( basedir, 'build.log' )
 assert buildLog.exists()
-assert buildLog.text.contains( "[INFO] Installing org.apache.maven.its.install.dae.fail:dae:1.0 at end" )
+assert buildLog.text.contains( "[INFO] Deferring install for org.apache.maven.its.install.dae.fail:dae:1.0 at end" )
 
diff --git a/src/it/install-at-end-pass/verify.groovy b/src/it/install-at-end-pass/verify.groovy
index 3487d52..f1b7e51 100644
--- a/src/it/install-at-end-pass/verify.groovy
+++ b/src/it/install-at-end-pass/verify.groovy
@@ -22,5 +22,5 @@
 
 File buildLog = new File( basedir, 'build.log' )
 assert buildLog.exists()
-assert buildLog.text.contains( "[INFO] Installing org.apache.maven.its.install.dae.pass:dae:1.0 at end" )
+assert buildLog.text.contains( "[INFO] Deferring install for org.apache.maven.its.install.dae.pass:dae:1.0 at end" )
 
diff --git a/src/main/java/org/apache/maven/plugins/install/InstallMojo.java b/src/main/java/org/apache/maven/plugins/install/InstallMojo.java
index b01c114..8ea2f30 100644
--- a/src/main/java/org/apache/maven/plugins/install/InstallMojo.java
+++ b/src/main/java/org/apache/maven/plugins/install/InstallMojo.java
@@ -20,19 +20,17 @@
  */
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.Map;
 
 import org.apache.maven.plugin.MojoExecutionException;
 import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugin.descriptor.PluginDescriptor;
 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.project.MavenProject;
-import org.apache.maven.project.ProjectBuildingRequest;
 import org.apache.maven.shared.transfer.artifact.install.ArtifactInstallerException;
 import org.apache.maven.shared.transfer.project.NoFileAssignedException;
 import org.apache.maven.shared.transfer.project.install.ProjectInstaller;
@@ -48,24 +46,15 @@
 public class InstallMojo
     extends AbstractInstallMojo
 {
-
-    /**
-     * When building with multiple threads, reaching the last project doesn't have to mean that all projects are ready
-     * to be installed
-     */
-    private static final AtomicInteger READYPROJECTSCOUNTER = new AtomicInteger();
-
-    private static final List<ProjectInstallerRequest> INSTALLREQUESTS =
-        Collections.synchronizedList( new ArrayList<ProjectInstallerRequest>() );
-
-    /**
-     */
     @Parameter( defaultValue = "${project}", readonly = true, required = true )
     private MavenProject project;
 
     @Parameter( defaultValue = "${reactorProjects}", required = true, readonly = true )
     private List<MavenProject> reactorProjects;
 
+    @Parameter( defaultValue = "${plugin}", required = true, readonly = true )
+    private PluginDescriptor pluginDescriptor;
+
     /**
      * Whether every project should be installed during its own install-phase or at the end of the multimodule build. If
      * set to {@code true} and the build fails, none of the reactor projects is installed.
@@ -88,56 +77,88 @@
     @Component
     private ProjectInstaller installer;
 
+    private enum State
+    {
+        SKIPPED, INSTALLED, TO_BE_INSTALLED
+    }
+
+    private static final String INSTALL_PROCESSED_MARKER = InstallMojo.class.getName() + ".processed";
+
+    private void putState( State state )
+    {
+        getPluginContext().put( INSTALL_PROCESSED_MARKER, state.name() );
+    }
+
+    private State getState( MavenProject project )
+    {
+        Map<String, Object> pluginContext = session.getPluginContext( pluginDescriptor, project );
+        return State.valueOf( (String) pluginContext.get( INSTALL_PROCESSED_MARKER ) );
+    }
+
+    private boolean hasState( MavenProject project )
+    {
+        Map<String, Object> pluginContext = session.getPluginContext( pluginDescriptor, project );
+        return pluginContext.containsKey( INSTALL_PROCESSED_MARKER );
+    }
+
     public void execute()
         throws MojoExecutionException, MojoFailureException
     {
-        boolean addedInstallRequest = false;
         if ( skip )
         {
             getLog().info( "Skipping artifact installation" );
+            putState( State.SKIPPED );
         }
         else
         {
-            // CHECKSTYLE_OFF: LineLength
-            ProjectInstallerRequest projectInstallerRequest =
-                new ProjectInstallerRequest().setProject( project );
-            // CHECKSTYLE_ON: LineLength
-
             if ( !installAtEnd )
             {
-                installProject( session.getProjectBuildingRequest(), projectInstallerRequest );
+                installProject( project );
+                putState( State.INSTALLED );
             }
             else
             {
-                INSTALLREQUESTS.add( projectInstallerRequest );
-                addedInstallRequest = true;
+                getLog().info( "Deferring install for " + getProjectReferenceId( project ) + " at end" );
+                putState( State.TO_BE_INSTALLED );
             }
         }
 
-        boolean projectsReady = READYPROJECTSCOUNTER.incrementAndGet() == reactorProjects.size();
-        if ( projectsReady )
+        if ( allProjectsMarked() )
         {
-            synchronized ( INSTALLREQUESTS )
+            for ( MavenProject reactorProject : reactorProjects )
             {
-                while ( !INSTALLREQUESTS.isEmpty() )
+                State state = getState( reactorProject );
+                if ( state == State.TO_BE_INSTALLED )
                 {
-                    installProject( session.getProjectBuildingRequest(), INSTALLREQUESTS.remove( 0 ) );
+                    installProject( reactorProject );
                 }
             }
         }
-        else if ( addedInstallRequest )
-        {
-            getLog().info( "Installing " + project.getGroupId() + ":" + project.getArtifactId() + ":"
-                + project.getVersion() + " at end" );
-        }
     }
 
-    private void installProject( ProjectBuildingRequest pbr, ProjectInstallerRequest pir )
+    private String getProjectReferenceId( MavenProject mavenProject )
+    {
+        return mavenProject.getGroupId() + ":" + mavenProject.getArtifactId() + ":" + mavenProject.getVersion();
+    }
+
+    private boolean allProjectsMarked()
+    {
+        for ( MavenProject reactorProject : reactorProjects )
+        {
+            if ( !hasState( reactorProject ) )
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void installProject( MavenProject pir )
         throws MojoFailureException, MojoExecutionException
     {
         try
         {
-            installer.install( session.getProjectBuildingRequest(), pir );
+            installer.install( session.getProjectBuildingRequest(), new ProjectInstallerRequest().setProject( pir ) );
         }
         catch ( IOException e )
         {
diff --git a/src/test/java/org/apache/maven/plugins/install/InstallMojoTest.java b/src/test/java/org/apache/maven/plugins/install/InstallMojoTest.java
index ed73d2c..1347e4b 100644
--- a/src/test/java/org/apache/maven/plugins/install/InstallMojoTest.java
+++ b/src/test/java/org/apache/maven/plugins/install/InstallMojoTest.java
@@ -19,17 +19,20 @@
  * under the License.
  */
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.io.File;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.maven.artifact.Artifact;
 import org.apache.maven.artifact.metadata.ArtifactMetadata;
 import org.apache.maven.execution.MavenSession;
 import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.descriptor.PluginDescriptor;
 import org.apache.maven.plugin.testing.AbstractMojoTestCase;
 import org.apache.maven.plugins.install.stubs.AttachedArtifactStub0;
 import org.apache.maven.plugins.install.stubs.InstallArtifactStub;
@@ -88,6 +91,8 @@
         MavenProject project = (MavenProject) getVariableValueFromObject( mojo, "project" );
         updateMavenProject( project );
 
+        setVariableValueToObject( mojo, "pluginContext", new ConcurrentHashMap<>() );
+        setVariableValueToObject( mojo, "pluginDescriptor", new PluginDescriptor() );
         setVariableValueToObject( mojo, "reactorProjects", Collections.singletonList( project ) );
         setVariableValueToObject( mojo, "session", createMavenSession() );
 
@@ -120,6 +125,8 @@
         MavenProject project = (MavenProject) getVariableValueFromObject( mojo, "project" );
         updateMavenProject( project );
 
+        setVariableValueToObject( mojo, "pluginContext", new ConcurrentHashMap<>() );
+        setVariableValueToObject( mojo, "pluginDescriptor", new PluginDescriptor() );
         setVariableValueToObject( mojo, "reactorProjects", Collections.singletonList( project ) );
         setVariableValueToObject( mojo, "session", createMavenSession() );
 
@@ -162,6 +169,8 @@
         MavenProject project = (MavenProject) getVariableValueFromObject( mojo, "project" );
         updateMavenProject( project );
 
+        setVariableValueToObject( mojo, "pluginContext", new ConcurrentHashMap<>() );
+        setVariableValueToObject( mojo, "pluginDescriptor", new PluginDescriptor() );
         setVariableValueToObject( mojo, "reactorProjects", Collections.singletonList( project ) );
         setVariableValueToObject( mojo, "session", createMavenSession() );
 
@@ -188,6 +197,8 @@
         MavenProject project = (MavenProject) getVariableValueFromObject( mojo, "project" );
         updateMavenProject( project );
 
+        setVariableValueToObject( mojo, "pluginContext", new ConcurrentHashMap<>() );
+        setVariableValueToObject( mojo, "pluginDescriptor", new PluginDescriptor() );
         setVariableValueToObject( mojo, "reactorProjects", Collections.singletonList( project ) );
         setVariableValueToObject( mojo, "session", createMavenSession() );
 
@@ -224,6 +235,8 @@
         MavenProject project = (MavenProject) getVariableValueFromObject( mojo, "project" );
         updateMavenProject( project );
 
+        setVariableValueToObject( mojo, "pluginContext", new ConcurrentHashMap<>() );
+        setVariableValueToObject( mojo, "pluginDescriptor", new PluginDescriptor() );
         setVariableValueToObject( mojo, "reactorProjects", Collections.singletonList( project ) );
         setVariableValueToObject( mojo, "session", createMavenSession() );
 
@@ -260,6 +273,8 @@
         MavenSession mavenSession = createMavenSession();
         updateMavenProject( project );
 
+        setVariableValueToObject( mojo, "pluginContext", new ConcurrentHashMap<>() );
+        setVariableValueToObject( mojo, "pluginDescriptor", new PluginDescriptor() );
         setVariableValueToObject( mojo, "reactorProjects", Collections.singletonList( project ) );
         setVariableValueToObject( mojo, "session", mavenSession );
 
@@ -316,6 +331,8 @@
         MavenProject project = (MavenProject) getVariableValueFromObject( mojo, "project" );
         updateMavenProject( project );
 
+        setVariableValueToObject( mojo, "pluginContext", new ConcurrentHashMap<>() );
+        setVariableValueToObject( mojo, "pluginDescriptor", new PluginDescriptor() );
         setVariableValueToObject( mojo, "reactorProjects", Collections.singletonList( project ) );
         setVariableValueToObject( mojo, "session", createMavenSession() );
 
@@ -357,6 +374,8 @@
         ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest();
         buildingRequest.setRepositorySession( repositorySession );
         when( session.getProjectBuildingRequest() ).thenReturn( buildingRequest );
+        when( session.getPluginContext(any(PluginDescriptor.class), any(MavenProject.class)))
+            .thenReturn( new ConcurrentHashMap<String, Object>() );
         return session;
     }