How to add new Maven lifecycle mapping

Tamás Cservenák

September 7, 2024

Ever repeating question from plugin/extension developers is “how to add new packaging” (in a “modern way”). For ages we did it by manually crafting plexus.xml in the plugin or extension JAR, that was not only error-prone but also tedious. But, indeed it had a great value as one could easily filter the XML (ie filtering plugin versions). But, plexus XML is plexus XML… yuck. So what now?

Here is an example from Eclipse Tycho: The tycho-maven-plugin originally defined this Plexus XML (and yes, it did filter for versions as well):

<component-set>
  <components>
    <component>
      <role>org.apache.maven.lifecycle.mapping.LifecycleMapping</role>
      <role-hint>p2-installable-unit</role-hint>
      <implementation>
        org.apache.maven.lifecycle.mapping.DefaultLifecycleMapping
      </implementation>
      <configuration>
        <lifecycles>
          <lifecycle>
            <id>default</id>
            <phases>
              <validate>
                org.eclipse.tycho:tycho-packaging-plugin:${project.version}:build-qualifier,
                org.eclipse.tycho:tycho-packaging-plugin:${project.version}:validate-id,
                org.eclipse.tycho:tycho-packaging-plugin:${project.version}:validate-version
              </validate>
              <initialize>
                org.eclipse.tycho:target-platform-configuration:${project.version}:target-platform
              </initialize>
              <process-resources>
                org.apache.maven.plugins:maven-resources-plugin:${resources-plugin.version}:resources
              </process-resources>
              <package>
                org.eclipse.tycho:tycho-packaging-plugin:${project.version}:package-iu,
                org.eclipse.tycho:tycho-p2-plugin:${project.version}:p2-metadata-default
              </package>
              <install>
                org.apache.maven.plugins:maven-install-plugin:${install-plugin.version}:install,
                org.eclipse.tycho:tycho-p2-plugin:${project.version}:update-local-index
              </install>
              <deploy>
                org.apache.maven.plugins:maven-deploy-plugin:${deploy-plugin.version}:deploy
              </deploy>
            </phases>
          </lifecycle>
        </lifecycles>
      </configuration>
    </component>
  </components>
</component-set>

So, how to migrate this off plexus XML?

We know Plexus XML “defines” components, managed by Plexus DI (and based by not so friendly Maven internal classes). Our goal would be then to create a JSR330 component. So, let’s create a “support class” first:

package org.eclipse.tycho.maven.lifecycle;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.maven.lifecycle.mapping.Lifecycle;
import org.apache.maven.lifecycle.mapping.LifecycleMapping;
import org.apache.maven.lifecycle.mapping.LifecyclePhase;

import javax.inject.Provider;

public abstract class LifecycleMappingProviderSupport implements Provider<LifecycleMapping> {

    private static final String DEFAULT_LIFECYCLE_KEY = "default";

    private final Lifecycle defaultLifecycle;
    private final LifecycleMapping lifecycleMapping;

    public LifecycleMappingProviderSupport() {
        this.defaultLifecycle = new Lifecycle();
        this.defaultLifecycle.setId(DEFAULT_LIFECYCLE_KEY);
        this.defaultLifecycle.setLifecyclePhases(loadMapping());

        this.lifecycleMapping = new LifecycleMapping() {
            @Override
            public Map<String, Lifecycle> getLifecycles() {
                return Collections.singletonMap(DEFAULT_LIFECYCLE_KEY, defaultLifecycle);
            }

            @Override
            public List<String> getOptionalMojos(String lifecycle) {
                return null;
            }

            @Override
            public Map<String, String> getPhases(String lifecycle) {
                if (DEFAULT_LIFECYCLE_KEY.equals(lifecycle)) {
                    return defaultLifecycle.getPhases();
                } else {
                    return null;
                }
            }
        };
    }

    private Map<String, LifecyclePhase> loadMapping() {
        Properties properties = new Properties();
        try (InputStream inputStream = getClass().getResourceAsStream(getClass().getSimpleName() + ".properties")) {
            properties.load(inputStream);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        HashMap<String, LifecyclePhase> result = new HashMap<>();
        for (String phase : properties.stringPropertyNames()) {
            result.put(phase, new LifecyclePhase(properties.getProperty(phase)));
        }
        return result;
    }

    @Override
    public LifecycleMapping get() {
        return lifecycleMapping;
    }
}

Using this support class, our actual mapping becomes “just” a simple empty class, that maps the component name (that is mapping name) onto data:

package org.eclipse.tycho.maven.plugin;

import org.eclipse.tycho.maven.lifecycle.LifecycleMappingProviderSupport;

import javax.inject.Named;
import javax.inject.Singleton;

@Singleton
@Named("p2-installable-unit")
public class P2InstallableUnitLifecycleMappingProvider extends LifecycleMappingProviderSupport {}

And we add the following Java Properties file to the same package where class above is. The “binary name” of the properties file should be org/eclipse/tycho/maven/plugin/P2InstallableUnitLifecycleMappingProvider.properties:

validate=org.eclipse.tycho:tycho-packaging-plugin:${project.version}:build-qualifier,\
  org.eclipse.tycho:tycho-packaging-plugin:${project.version}:validate-id,\
  org.eclipse.tycho:tycho-packaging-plugin:${project.version}:validate-version
initialize=org.eclipse.tycho:target-platform-configuration:${project.version}:target-platform
process-resources=org.apache.maven.plugins:maven-resources-plugin:${resources-plugin.version}:resources
package=org.eclipse.tycho:tycho-packaging-plugin:${project.version}:package-iu,\
  org.eclipse.tycho:tycho-p2-plugin:${project.version}:p2-metadata-default
install=org.apache.maven.plugins:maven-install-plugin:${install-plugin.version}:install,\
  org.eclipse.tycho:tycho-p2-plugin:${project.version}:update-local-index
deploy=org.apache.maven.plugins:maven-deploy-plugin:${deploy-plugin.version}:deploy

And finally, all we need to do is to enable filtering on resources. And we have the very same effect as with huge Plexus XML.

To verify ourselves, just add a small UT, just to check the things:

package org.eclipse.tycho.maven.plugin;

import org.apache.maven.lifecycle.mapping.LifecycleMapping;
import org.apache.maven.lifecycle.mapping.LifecycleMojo;
import org.apache.maven.lifecycle.mapping.LifecyclePhase;
import org.eclipse.sisu.launch.Main;
import org.junit.jupiter.api.Test;

import javax.inject.Inject;
import javax.inject.Named;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;

@Named
public class LifecycleMappingTest {
    @Inject
    private Map<String, LifecycleMapping> lifecycleMappings;

    @Test
    void smoke() {
        LifecycleMappingTest self = Main.boot(LifecycleMappingTest.class);
        assertEquals(7, self.lifecycleMappings.size());
        System.out.println("All mappings defined in this plugin:");
        for (Map.Entry<String, LifecycleMapping> mapping : self.lifecycleMappings.entrySet()) {
            System.out.println("* " + mapping.getKey());
            for (Map.Entry<String, LifecyclePhase> phases : mapping.getValue().getLifecycles().get("default").getLifecyclePhases().entrySet()) {
                System.out.println("  " + phases.getKey());
                for (LifecycleMojo mojo : phases.getValue().getMojos()) {
                    System.out.println("   -> " + mojo.getGoal());
                }
            }
        }
    }
}

Enjoy!