You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by st...@apache.org on 2020/03/18 13:10:01 UTC

[maven] 01/06: [MNG-5668] Proof of concept implementation of dynamic phases

This is an automated email from the ASF dual-hosted git repository.

stephenc pushed a commit to branch mng-5668-poc
in repository https://gitbox.apache.org/repos/asf/maven.git

commit c107911816f4b62ca5f2c9ca883ad336bf856976
Author: Stephen Connolly <st...@gmail.com>
AuthorDate: Mon Nov 11 18:46:52 2019 +0000

    [MNG-5668] Proof of concept implementation of dynamic phases
---
 .../internal/DefaultLifecycleMappingDelegate.java  |  38 +++-
 .../maven/lifecycle/internal/MojoExecutor.java     |  49 +++++-
 .../maven/lifecycle/internal/PhaseComparator.java  |  84 +++++++++
 .../lifecycle/internal/PhaseExecutionPoint.java    |  54 ++++++
 .../apache/maven/lifecycle/internal/PhaseId.java   | 196 +++++++++++++++++++++
 .../maven/lifecycle/internal/PhaseRecorder.java    |  12 +-
 maven-plugin-api/src/main/mdo/lifecycle.mdo        |  62 ++++++-
 7 files changed, 474 insertions(+), 21 deletions(-)

diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleMappingDelegate.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleMappingDelegate.java
index 892e4f1..a8c6c4b 100644
--- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleMappingDelegate.java
+++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleMappingDelegate.java
@@ -19,12 +19,6 @@ package org.apache.maven.lifecycle.internal;
  * under the License.
  */
 
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
 import org.apache.maven.execution.MavenSession;
 import org.apache.maven.lifecycle.Lifecycle;
 import org.apache.maven.lifecycle.LifecycleMappingDelegate;
@@ -42,6 +36,12 @@ import org.apache.maven.project.MavenProject;
 import org.codehaus.plexus.component.annotations.Component;
 import org.codehaus.plexus.component.annotations.Requirement;
 
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
 /**
  * Lifecycle mapping delegate component interface. Calculates project build execution plan given {@link Lifecycle} and
  * lifecycle phase. Standard lifecycles use plugin execution {@code <phase>} or mojo default lifecycle phase to
@@ -67,7 +67,7 @@ public class DefaultLifecycleMappingDelegate
          */
 
         Map<String, Map<Integer, List<MojoExecution>>> mappings =
-            new LinkedHashMap<>();
+            new TreeMap<>( new PhaseComparator( lifecycle.getPhases() ) );
 
         for ( String phase : lifecycle.getPhases() )
         {
@@ -96,7 +96,8 @@ public class DefaultLifecycleMappingDelegate
                 // to examine the phase it is associated to.
                 if ( execution.getPhase() != null )
                 {
-                    Map<Integer, List<MojoExecution>> phaseBindings = mappings.get( execution.getPhase() );
+                    Map<Integer, List<MojoExecution>> phaseBindings =
+                        getPhaseBindings( mappings, execution.getPhase() );
                     if ( phaseBindings != null )
                     {
                         for ( String goal : execution.getGoals() )
@@ -116,7 +117,8 @@ public class DefaultLifecycleMappingDelegate
                             pluginManager.getMojoDescriptor( plugin, goal, project.getRemotePluginRepositories(),
                                                              session.getRepositorySession() );
 
-                        Map<Integer, List<MojoExecution>> phaseBindings = mappings.get( mojoDescriptor.getPhase() );
+                        Map<Integer, List<MojoExecution>> phaseBindings =
+                            getPhaseBindings( mappings, mojoDescriptor.getPhase() );
                         if ( phaseBindings != null )
                         {
                             MojoExecution mojoExecution = new MojoExecution( mojoDescriptor, execution.getId() );
@@ -146,6 +148,24 @@ public class DefaultLifecycleMappingDelegate
 
     }
 
+    private Map<Integer, List<MojoExecution>> getPhaseBindings( Map<String, Map<Integer, List<MojoExecution>>> mappings,
+                                                                String phase )
+    {
+        Map<Integer, List<MojoExecution>> result = mappings.get( phase );
+        if ( result == null )
+        {
+            // check if this is related to an phase in the plan (pre/post or different priority)
+            PhaseId id = PhaseId.of( phase );
+            if ( mappings.containsKey( id.phase() ) )
+            {
+                // lazy add the phases we need
+                result = new TreeMap<>();
+                mappings.put( phase, result );
+            }
+        }
+        return result;
+    }
+
     private void addMojoExecution( Map<Integer, List<MojoExecution>> phaseBindings, MojoExecution mojoExecution,
                                    int priority )
     {
diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
index b78f54d..df3f34c 100644
--- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
+++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
@@ -44,6 +44,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -143,9 +144,50 @@ public class MojoExecutor
 
         PhaseRecorder phaseRecorder = new PhaseRecorder( session.getCurrentProject() );
 
-        for ( MojoExecution mojoExecution : mojoExecutions )
+        Iterator<MojoExecution> iterator = mojoExecutions.iterator();
+        try
         {
-            execute( session, mojoExecution, projectIndex, dependencyContext, phaseRecorder );
+            while ( iterator.hasNext() )
+            {
+                MojoExecution mojoExecution = iterator.next();
+                execute( session, mojoExecution, projectIndex, dependencyContext, phaseRecorder );
+            }
+        }
+        catch ( LifecycleExecutionException failure )
+        {
+            // run any post: executions for the current phase
+            while ( iterator.hasNext() )
+            {
+                MojoExecution mojoExecution = iterator.next();
+                String lifecyclePhase = mojoExecution.getLifecyclePhase();
+                if ( lifecyclePhase == null )
+                {
+                    // we have reached an execution that is not bound to a phase, thus there is no post: for last
+                    // executed phase
+                    break;
+                }
+                if ( phaseRecorder.isDifferentPhase( mojoExecution ) )
+                {
+                    // this is a different phase from the last executed phase, thus no more post:
+                    break;
+                }
+                PhaseId phaseId = PhaseId.of( lifecyclePhase );
+                if ( phaseId.executionPoint() != PhaseExecutionPoint.AFTER )
+                {
+                    // only interested in post: executions
+                    continue;
+                }
+                try
+                {
+                    execute( session, mojoExecution, projectIndex, dependencyContext, phaseRecorder );
+                }
+                catch ( LifecycleExecutionException postFailure )
+                {
+                    // failures are tagged as suppressed
+                    failure.addSuppressed( postFailure );
+                }
+            }
+            throw failure;
         }
     }
 
@@ -209,8 +251,7 @@ public class MojoExecutor
             {
                 pluginManager.executeMojo( session, mojoExecution );
             }
-            catch ( MojoFailureException | PluginManagerException | PluginConfigurationException
-                | MojoExecutionException e )
+            catch ( MojoFailureException | PluginManagerException | PluginConfigurationException | MojoExecutionException e )
             {
                 throw new LifecycleExecutionException( mojoExecution, session.getCurrentProject(), e );
             }
diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseComparator.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseComparator.java
new file mode 100644
index 0000000..3670c28
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseComparator.java
@@ -0,0 +1,84 @@
+package org.apache.maven.lifecycle.internal;
+
+/*
+ * 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.Comparator;
+import java.util.List;
+
+/**
+ * Compares phases within the context of a specific lifecycle with secondary sorting based on the {@link PhaseId}.
+ */
+public class PhaseComparator
+    implements Comparator<String>
+{
+    /**
+     * The lifecycle phase ordering.
+     */
+    private final List<String> lifecyclePhases;
+
+    /**
+     * Constructor.
+     *
+     * @param lifecyclePhases the lifecycle phase ordering.
+     */
+    public PhaseComparator( List<String> lifecyclePhases )
+    {
+        this.lifecyclePhases = lifecyclePhases;
+    }
+
+    @Override
+    public int compare( String o1, String o2 )
+    {
+        PhaseId p1 = PhaseId.of( o1 );
+        PhaseId p2 = PhaseId.of( o2 );
+        int i1 = lifecyclePhases.indexOf( p1.phase() );
+        int i2 = lifecyclePhases.indexOf( p2.phase() );
+        if ( i1 == -1 && i2 == -1 )
+        {
+            // unknown phases, leave in existing order
+            return 0;
+        }
+        if ( i1 == -1 )
+        {
+            // second one is known, so it comes first
+            return 1;
+        }
+        if ( i2 == -1 )
+        {
+            // first one is known, so it comes first
+            return -1;
+        }
+        int rv = Integer.compare( i1, i2 );
+        if ( rv != 0 )
+        {
+            return rv;
+        }
+        // same phase, now compare execution points
+        i1 = p1.executionPoint().ordinal();
+        i2 = p2.executionPoint().ordinal();
+        rv = Integer.compare( i1, i2 );
+        if ( rv != 0 )
+        {
+            return rv;
+        }
+        // same execution point, now compare priorities (highest wins, so invert)
+        return -Integer.compare( p1.priority(), p2.priority() );
+    }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseExecutionPoint.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseExecutionPoint.java
new file mode 100644
index 0000000..65313a5
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseExecutionPoint.java
@@ -0,0 +1,54 @@
+package org.apache.maven.lifecycle.internal;
+
+/*
+ * 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.
+ */
+
+/**
+ * Represents where a dynamic phase should be executed within a static phase.
+ */
+public enum PhaseExecutionPoint
+{
+    /**
+     * Execution must occur before any executions of the phase proper. Failure of any {@link #BEFORE} dynamic phase
+     * execution will prevent the {@link #AS} phase but will not prevent any {@link #AFTER} dynamic phases.
+     */
+    BEFORE( "before:" ),
+    /**
+     * Execution is the execution of the phase proper. Failure of any {@link #AS} dynamic phase execution will fail
+     * the phase. Any {@link #AFTER} phases will still be execution.
+     */
+    AS( "" ),
+    /**
+     * Guaranteed execution dynamic phases on completion of the static phase. All {@link #AFTER} dynamic phases will
+     * be executed provided at least one {@link #BEFORE} or {@link #AS} dynamic phase has started execution.
+     */
+    AFTER( "after:" );
+
+    private final String prefix;
+
+    PhaseExecutionPoint( String prefix )
+    {
+        this.prefix = prefix;
+    }
+
+    public String prefix()
+    {
+        return prefix;
+    }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseId.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseId.java
new file mode 100644
index 0000000..646ac56
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseId.java
@@ -0,0 +1,196 @@
+package org.apache.maven.lifecycle.internal;
+
+/*
+ * 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.Map;
+import java.util.WeakHashMap;
+
+/**
+ * Represents a parsed phase identifier.
+ */
+public class PhaseId
+{
+    /**
+     * Interned {@link PhaseId} instances.
+     */
+    private static final Map<String, PhaseId> instances = new WeakHashMap<>();
+
+    /**
+     * The execution point of this {@link PhaseId}.
+     */
+    private final PhaseExecutionPoint executionPoint;
+
+    /**
+     * The static phase that this dynamic phase belongs to.
+     */
+    private final String phase;
+
+    /**
+     * The priority of this dynamic phase within the static phase.
+     */
+    private final int priority;
+
+    /**
+     * Parses the phase identifier.
+     *
+     * @param phase the phase identifier.
+     * @return the {@link PhaseId}.
+     */
+    public static synchronized PhaseId of( String phase )
+    {
+        PhaseId result = instances.get( phase );
+        if ( result == null )
+        {
+            result = new PhaseId( phase );
+            instances.put( phase, result );
+        }
+        return result;
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param phase the phase identifier string.
+     */
+    private PhaseId( String phase )
+    {
+        int executionPointEnd = phase.indexOf( ':' );
+        int phaseStart;
+        if ( executionPointEnd == -1 )
+        {
+            executionPoint = PhaseExecutionPoint.AS;
+            phaseStart = 0;
+        }
+        else
+        {
+            switch ( phase.substring( 0, executionPointEnd ) )
+            {
+                case "before":
+                    executionPoint = PhaseExecutionPoint.BEFORE;
+                    phaseStart = executionPointEnd + 1;
+                    break;
+                case "after":
+                    executionPoint = PhaseExecutionPoint.AFTER;
+                    phaseStart = executionPointEnd + 1;
+                    break;
+                default:
+                    executionPoint = PhaseExecutionPoint.AS;
+                    phaseStart = 0;
+                    break;
+            }
+        }
+        int phaseEnd = phase.indexOf( '[' );
+        if ( phaseEnd == -1 )
+        {
+            priority = 0;
+            this.phase = phase.substring( phaseStart );
+        }
+        else
+        {
+            int priorityEnd = phase.lastIndexOf( ']' );
+            boolean havePriority;
+            int priority;
+            if ( priorityEnd < phaseEnd + 1 )
+            {
+                priority = 0;
+                havePriority = false;
+            }
+            else
+            {
+                try
+                {
+                    priority = Integer.parseInt( phase.substring( phaseEnd + 1, priorityEnd ) );
+                    havePriority = true;
+                }
+                catch ( NumberFormatException e )
+                {
+                    // priority must be an integer
+                    priority = 0;
+                    havePriority = false;
+                }
+            }
+            if ( havePriority )
+            {
+                this.phase = phase.substring( phaseStart, phaseEnd );
+                this.priority = priority;
+            }
+            else
+            {
+                this.phase = phase.substring( phaseStart );
+                this.priority = 0;
+            }
+        }
+    }
+
+    @Override
+    public boolean equals( Object o )
+    {
+        if ( this == o )
+        {
+            return true;
+        }
+        if ( o == null || getClass() != o.getClass() )
+        {
+            return false;
+        }
+
+        PhaseId phaseId = (PhaseId) o;
+
+        if ( priority() != phaseId.priority() )
+        {
+            return false;
+        }
+        if ( executionPoint() != phaseId.executionPoint() )
+        {
+            return false;
+        }
+        return phase().equals( phaseId.phase() );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int result = executionPoint().hashCode();
+        result = 31 * result + phase().hashCode();
+        result = 31 * result + priority();
+        return result;
+    }
+
+    @Override
+    public String toString()
+    {
+        return executionPoint().prefix() + phase() + ( priority() != 0 ? "[" + priority() + ']' : "" );
+    }
+
+    public PhaseExecutionPoint executionPoint()
+    {
+        return executionPoint;
+    }
+
+    public String phase()
+    {
+        return phase;
+    }
+
+    public int priority()
+    {
+        return priority;
+    }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseRecorder.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseRecorder.java
index 3316c50..c76f22f 100644
--- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseRecorder.java
+++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseRecorder.java
@@ -24,9 +24,10 @@ import org.apache.maven.project.MavenProject;
 
 /**
  * <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
- * @since 3.0
+ *
  * @author Benjamin Bentmann
  * @author Kristian Rosenvold
+ * @since 3.0
  */
 public class PhaseRecorder
 {
@@ -45,14 +46,15 @@ public class PhaseRecorder
 
         if ( lifecyclePhase != null )
         {
+            PhaseId phaseId = PhaseId.of( lifecyclePhase );
             if ( lastLifecyclePhase == null )
             {
-                lastLifecyclePhase = lifecyclePhase;
+                lastLifecyclePhase = phaseId.phase();
             }
-            else if ( !lifecyclePhase.equals( lastLifecyclePhase ) )
+            else if ( !phaseId.phase().equals( lastLifecyclePhase ) )
             {
                 project.addLifecyclePhase( lastLifecyclePhase );
-                lastLifecyclePhase = lifecyclePhase;
+                lastLifecyclePhase = phaseId.phase();
             }
         }
 
@@ -69,7 +71,7 @@ public class PhaseRecorder
         {
             return lastLifecyclePhase != null;
         }
-        return !lifecyclePhase.equals( lastLifecyclePhase );
+        return !PhaseId.of( lifecyclePhase ).phase().equals( lastLifecyclePhase );
 
     }
 
diff --git a/maven-plugin-api/src/main/mdo/lifecycle.mdo b/maven-plugin-api/src/main/mdo/lifecycle.mdo
index 7dfce74..b86fde8 100644
--- a/maven-plugin-api/src/main/mdo/lifecycle.mdo
+++ b/maven-plugin-api/src/main/mdo/lifecycle.mdo
@@ -17,8 +17,8 @@ specific language governing permissions and limitations
 under the License.
 -->
 
-<model xmlns="http://codehaus-plexus.github.io/MODELLO/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://codehaus-plexus.github.io/MODELLO/1.0.0 http://codehaus-plexus.github.io/modello/xsd/modello-1.0.0.xsd"
+<model xmlns="http://codehaus-plexus.github.io/MODELLO/1.8.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="https://codehaus-plexus.github.io/MODELLO/1.8.0 https://codehaus-plexus.github.io/modello/xsd/modello-1.8.0.xsd"
   xml.namespace="http://maven.apache.org/LIFECYCLE/${version}"
   xml.schemaLocation="http://maven.apache.org/xsd/lifecycle-${version}.xsd">
   <id>lifecycle-mappings</id>
@@ -79,13 +79,29 @@ under the License.
       <version>1.0.0</version>
       <description>A phase mapping definition.</description>
       <fields>
-        <field>
+        <field java.getter="false">
           <name>id</name>
           <required>true</required>
           <version>1.0.0</version>
           <type>String</type>
           <description>The ID of this phase, e.g., &lt;code&gt;generate-sources&lt;/code&gt;.</description>
         </field>
+        <field xml.attribute="true">
+          <name>executionPoint</name>
+          <required>false</required>
+          <version>1.0.0</version>
+          <type>String</type>
+          <defaultValue><![CDATA[]]></defaultValue>
+          <description><![CDATA[If specified, identifies this phase as a dynamic phase to decorate the specified phase id, e.g. <code>after</code> or <code>before</code>.]]></description>
+        </field>
+        <field xml.attribute="true">
+          <name>priority</name>
+          <required>false</required>
+          <version>1.0.0</version>
+          <type>int</type>
+          <defaultValue>0</defaultValue>
+          <description>If specified, identifies a within phase prioritization of executions.</description>
+        </field>
         <field>
           <name>executions</name>
           <version>1.0.0</version>
@@ -102,6 +118,46 @@ under the License.
           <description>Configuration to pass to all goals run in this phase.</description>
         </field>
       </fields>
+      <codeSegments>
+        <codeSegment>
+          <version>1.0.0</version>
+          <code><![CDATA[
+    /**
+     * Get the ID of this phase, e.g.,
+     * <code>generate-sources</code>.
+     *
+     * @return String
+     */
+    public String getRawId()
+    {
+        return id;
+    }
+
+    /**
+     * Get the effective ID of this phase, e.g.,
+     * <code>generate-sources</code> or <code>after:integration-test[1000]</code>.
+     *
+     * @return String
+     */
+    public String getId()
+    {
+        if ( executionPoint == null )
+        {
+            if ( priority == 0 )
+            {
+                return id;
+            }
+            return id + '[' + priority + ']';
+        }
+        if ( priority == 0 )
+        {
+            return executionPoint + ':' + id;
+        }
+        return executionPoint + ':' + id + '[' + priority + ']';
+    }
+]]></code>
+        </codeSegment>
+      </codeSegments>
     </class>
     <class>
       <name>Execution</name>