Developing a M2E 1.0.0 Configuration Plugin

I recently had to go through upgrading from Sonatype M2Eclipse 0.12 to Eclipse M2E 1.0.0.   A lot of stuff was fairly easy to get working again, as package names and extension point ids where under new package and ids.   The item that was more difficult to do was get the configuration plugin working for our projects.    The new configuration extension point not only changed it’s ids, but also what attributes it accepted, and in addition it now also contributes to the build life cycle.

Lifecycle Changes

Prior to 1.0.0, m2eclipse integration in the build lifecycle, was a mass of praying and hoping that you did not get into a build loop.   Certain versions of m2eclipse were notorious for causing build loops as both Maven and Eclipse both wanted to control the build cycle.   To help eliminate this problem, M2E 1.0.0 introduced the Lifecycle configuration.   You must now specifically tell M2E what it is to do during a build and project configuration.   This has the side affect that if it does not understand or know what to do with a particular maven plugin, it will not run that plugin during the build within the IDE.

This can cause your initial import of projects that ran fine in 0.12 to show compilation errors, when imported with 1.0.0.   This mainly affects code-generation plugins or any plugin that runs before compilation phase.  There are some documented ways to get existing plugins to run on the “M2E plugin execution not covered” wiki page.     While this can work for some plugins, those that do code-generation need some more help.   For this, there is the M2E Extension Development wiki page.

It should be noted that there is a slowly growning list of plugin configurations for M2E 1.0.0, and these can be found in the M2E Discovery Marketplace.   When importing an existing Maven Project, M2E will try and find a plugin configuration extension for you, if one is not already installed in your eclipse environment or configured in your pom.   Given time the annoyance of the Lifecycle Mapping issue will eventually go away.

One gotcha is that if you specify the configuration in the POM it takes precedence over your eclipse extension point configuration.

Developing an M2E extension

For the Turmeric SOA project, we have a couple of custom maven plugins that help with code-generation of the Services, Type Libraries, and Error Libraries.   With M2E 1.0.0, these would no longer run in eclipse, so we need to create a custom configuration.  Since we need to add additional source folders to the build path, this currently requires that we do this during the plugin configuration and after it executes.

First, we need to let M2E know that we are contributing a configuration and an lifecyclemapping file as well.

   <extension
        point="org.eclipse.m2e.core.projectConfigurators">
     <configurator
           class="org.ebayopensource.turmeric.eclipse.maven.sconfig.TurmerStandardProjectConfigurator"
           id="org.ebayopensource.turmeric.eclipse.maven.turmericMavenConfigurator"
           name="Turmeric Maven Standard Configurator">
     </configurator>
    </extension>

    <extension
         point="org.eclipse.m2e.core.lifecycleMappingMetadataSource">
    </extension>

The above tells the configuration extension point that we will be providing the TurmerStandardProjectConfigurator class for our contribution, and that additional information for lifecycle mappoing, can be found in the lifecycle-mapping-metada.xml file.

Second the lifecycle-mapping-metada.xml we will be contributing must be in the root of your eclipse plugin, and it should look similar to the following.

<lifecycleMappingMetadata>
   <pluginExecutions>
     <pluginExecution>
       <pluginExecutionFilter>
           <groupId>org.ebayopensource.turmeric.maven</groupId>
           <artifactId>turmeric-maven-plugin</artifactId>
           <versionRange>[0.9.0,)</versionRange>
           <goals>
             <goal>gen-interface-wsdl</goal>
             <goal>gen-implementation</goal>
             <goal>gen-typelibrary</goal>
             <goal>gen-errorlibrary</goal>
           </goals>
       </pluginExecutionFilter>
       <action>
          <configurator>
              <id>org.ebayopensource.turmeric.eclipse.maven.turmericMavenConfigurator</id>
          </configurator>
       </action>
    </pluginExecution>
 <pluginExecutions>
</lifecycleMappingMetadata>

The above is a shortened version of the actual lifecycle mapping file we use for the Turmeric SOA configuration. It basically specifies, the version range of the plugins that this configuration handles, the goals that it should handle, and finally the action to be taken. In this case we will defer to the Projects configurator. Note that you must specify the Id that you specified in the extension point that was created earlier.

The next bit of magic that needs to be done is to write the configurator. For most projects, you’ll want to extend from the AbstractJavaProjectConfigurator class provided by the m2e.jdt plugin. This class contains the standard items that most java projects will need. For Turmeric we needed to provide a couple of different items.

1. Add some additional natures to the .project file.
2. Specify additional source paths to be added.

	@Override
	public void configureRawClasspath(ProjectConfigurationRequest request,
			IClasspathDescriptor classpath, IProgressMonitor monitor)
			throws CoreException {

		IProject project = request.getProject();
		IMavenProjectFacade facade = request.getMavenProjectFacade();
		List<IPath> additionalSrcDirs = new ArrayList<IPath>();

		if (isErrorLibProject(request) || isInterfaceProject(request)
				|| isTypeLibProject(request)
				|| isImplementationProject(request)) {
			additionalSrcDirs.add(new Path(project.getFullPath().toString() + GENERATED_SOURCES_CODEGEN));
			additionalSrcDirs
					.add(new Path(project.getFullPath().toString() + GENERATED_RESOURCES_CODEGEN));
		} else {
			additionalSrcDirs.add(new Path(project.getFullPath().toString() +
					GENERATED_SOURCES_JAXB_EPISODE));
			additionalSrcDirs.add(new Path(project.getFullPath().toString() +
					GENERATED_RESOURCES_JAXB_EPISODE));
		}

		for (IPath path : additionalSrcDirs) {
				if (!classpath.containsPath(path)) {
					classpath.addSourceEntry(path,
							facade.getOutputLocation(), true);
			}
		}
	}

	@Override
	public void configure(ProjectConfigurationRequest projRequest,
			IProgressMonitor monitor) throws CoreException {

		SupportedProjectType projectType = null;
		IProject project = projRequest.getProject();
		if (isInterfaceProject(projRequest)) {
			projectType = SupportedProjectType.INTERFACE;
		} else if (isImplementationProject(projRequest)) {
			projectType = SupportedProjectType.IMPL;
		} else if (isErrorLibProject(projRequest)) {
			projectType = SupportedProjectType.ERROR_LIBRARY;
		} else if (isTypeLibProject(projRequest)) {
			projectType = SupportedProjectType.TYPE_LIBRARY;
		} else if (isConsumerLibProject(projRequest)) {
			projectType = SupportedProjectType.CONSUMER;
		} else {
			return;
		}

		String natureId = GlobalRepositorySystem.instanceOf()
				.getActiveRepositorySystem().getProjectNatureId(projectType);

		JDTUtil.addNatures(project, monitor, natureId);
	}

In addition, we need to contribute to the build execution phase of the lifecycle.

@Override
public AbstractBuildParticipant getBuildParticipant(
   IMavenProjectFacade projectFacade, MojoExecution execution,
   IPluginExecutionMetadata executionMetadata) {
   return new TurmericStandardBuildParticipant(execution);
}

The TurmericStandardBuildParticipant class handles whether the maven plugin should be executued, and if so any precondition or post condition items that must be met.

public class TurmericStandardBuildParticipant extends
		MojoExecutionBuildParticipant {

	private static final String GENERATED_SOURCES = "target/generated-sources";

	/**
	 * Instantiates a new turmeric standard build participant.
	 *
	 * @param execution
	 *            the execution
	 */
	public TurmericStandardBuildParticipant(MojoExecution execution) {
		super(execution, true);
	}

	@Override
	public Set<IProject> build(int kind, IProgressMonitor monitor)
			throws Exception {

		Set<IProject> sproj = super.build(kind, monitor);

		BuildContext buildContext = getBuildContext();
		IMavenProjectFacade mproj = getMavenProjectFacade();

		IProject proj = mproj.getProject();

		proj.refreshLocal(IResource.DEPTH_INFINITE, monitor);

		IFile generatedSource = proj.getFile(GENERATED_SOURCES);
			File generatedSourceFolder = generatedSource.getFullPath().toFile();
			buildContext.refresh(generatedSourceFolder);

		IFile generatedResource = proj.getFile(GENERATED_SOURCES);
			File generatedResourceFolder = generatedResource.getFullPath()
					.toFile();
			buildContext.refresh(generatedResourceFolder);

		return sproj;

	}
}

In this case, after execution of the plugin, we need to force it to refresh the contents of the generated-sources and generated-resources directories. If we do not do this, eclipse will not see these files. That is basically it for the build participant. So how do we test this to make sure everything works and we don’t run into any regressions later?

Testing M2E Configuration Extensions

Sonatype does provide a couple of examples, in the m2e-core-tests.git repository, and I recommend either extending or tweaking from the Antlr test they have in that repository. A sample test for the turmeric configurator is below:

public class TurmericStandardProjectConfiguratorTest extends
		AbstractMavenProjectTestCase {

	private IProject buildProject(String testProjectPOMPath)
			throws IOException, CoreException, InterruptedException {
		ResolverConfiguration configuration = new ResolverConfiguration();
		IProject project = importProject(testProjectPOMPath, configuration);
		waitForJobsToComplete();

		project.build(IncrementalProjectBuilder.FULL_BUILD, monitor);
		project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor);
		waitForJobsToComplete();
		return project;
	}

	private void assertSourcePathFound(IClasspathEntry[] cp, Path sourcePath) {
		boolean fndsw = false;
		for (IClasspathEntry entry : cp) {
			if (entry.getPath().equals(sourcePath)) {
				fndsw = true;
				break;
			}
		}
		assertTrue("Missing " + sourcePath.toString() + "source entry", fndsw);
	}

	private IClasspathEntry[] getClasspath(IProject project)
			throws JavaModelException {
		IJavaProject javaProject1 = JavaCore.create(project);
		IClasspathEntry[] cp = javaProject1.getRawClasspath();
		return cp;
	}

	@Test
	public void testJAXBEpisodeCodegenToolsProject() throws Exception {
		String testProjectPOMPath = "projects/codegen-tools/pom.xml";
		IProject project = buildProject(testProjectPOMPath);

		IClasspathEntry[] cp = getClasspath(project);

		Path sourcePath = new Path(
				"/codegen-tools/target/generated-sources/jaxb-episode");
		assertSourcePathFound(cp, sourcePath);

		sourcePath = new Path(
				"/codegen-tools/target/generated-resources/jaxb-episode");
		assertSourcePathFound(cp, sourcePath);

		String file = "target/generated-sources/jaxb-episode/org/ebayopensource/turmeric/runtime/codegen/common/CodeGenOptionType.java";
		assertTrue(project.getFile(file).isSynchronized(IResource.DEPTH_ZERO));
		assertTrue(project.getFile(file).isAccessible());
	}
}

The above code extends from the AbstractMavenProjectTestCase, and tests importing a maven project, allowing the execution of the plugin to occur, and then checking the results. These are integration tests, and not unit tests in the purest form as they require you to run an eclipse ide to do this. I have setup our tests as part of the tycho build we use, so these are run automatically as part of our build process.

Hopefully, this helps others.  The source for this can be found in my fork of the Turmeric SOA Eclipse Plugins on github.

Advertisements
This entry was posted in agile, eclipse, maven, testing, turmeric. Bookmark the permalink.

6 Responses to Developing a M2E 1.0.0 Configuration Plugin

  1. Pingback: Dave Carver: Developing a M2E 1.0.0 Configuration Plugin

  2. jeffmaury says:

    Nice article. However, as I want to write unit tests for my GWT configurator, I can’t find the org.eclipse.m2e.tests.common bundle. I installed M2E from the indigo update site.
    Can you help ?

    Regards
    Jeff

  3. jeffmaury says:

    What I was looking was a P2 repository where this bundle is present. I will ask on the m2e mailing list. I understand that you copied the source foryour own purpose.

    Thanks

  4. Ivan dal Busco says:

    Nice article, thanks a lot.

    I came across exactly the same troubles as you. When updating from Eclipse 3.6 to 3.7 (and thus from m2eclipse 0.12 to m2e 1.0.0), I lost my automatic binding class generation performed by a
    org.codehaus.mojo:jaxb2-maven-plugin configuration in my pom file. So I added an org.eclipse.m2e:lifecycle-mapping configuration, which effectively generated the classes in
    directory target/generated-sources/jaxb. However Eclipse still displays compilation errors, because the directory is not a “Source folder” of the project. Can I add a Source folder configuration to my pom, or do I have to go through the quite involved process of writing an extension to AbstractJavaProjectConfigurator?

    Thanks in advance for any hint.

    Ivan

    • kingargyle says:

      You might be able use the sources entry for the build, but the best way is to specifically write an m2e build contribution extension that adds the directories.

      There is some discussion about making a more generic source generation configurator, but nothing concrete yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s