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 a3eddecda2e0a7e59ef2712303079fd0c36b89f4 Author: Stephen Connolly <stephen.alan.conno...@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 1ddee05..12710d6 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., <code>generate-sources</code>.</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>