Tuesday, October 9, 2012

Integrating a custom builder

A builder can be used to trigger custom actions during a project build. You can use it to update resource files, generate documentation or to twitter every piece of code you write...

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.

Step 1: Creating the builder

This is the easy part. Create a new Plug-in project named com.codeandme.custombuilder and switch to the Extensions tab of the plugin.xml.

Add an extension for org.eclipse.core.resources.builders. Set the ID to com.codeandme.custombuilder.myBuilder, leave the builder settings empty and create a run entry below. There set the class to com.codeandme.custombuilder.MyBuilder. Implement the class with following code:
package com.codeandme.custombuilder.builders;

import java.util.Map;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;

public class MyBuilder extends IncrementalProjectBuilder {

 public static final String BUILDER_ID = "com.codeandme.custombuilder.myBuilder";

 @Override
 protected IProject[] build(final int kind, final Map<String, String> args, final IProgressMonitor monitor)
   throws CoreException {

  System.out.println("Custom builder triggered");

  // get the project to build
  getProject();

  switch (kind) {

  case FULL_BUILD:
   break;

  case INCREMENTAL_BUILD:
   break;

  case AUTO_BUILD:
   break;
  }

  return null;
 }
}

Do not forget to add a plug-in dependency for org.eclipse.core.runtime.

Your builder is done. Sure you need to add functionality to it, but this is your part. So now what? We need to add the builder to projects. To selectively add a builder eclipse suggests to use the Configure entry in the popup menu of projects.

Step 2: Create context menu entries

First lets create the commands to add and remove our builder.

Add the command definitions to your plugin.xml

      <command
            defaultHandler="com.codeandme.custombuilder.commands.AddBuilder"
            id="com.codeandme.custombuilder.addBuilder"
            name="Add Custom Builder">
      </command>
      <command
            defaultHandler="com.codeandme.custombuilder.commands.RemoveBuilder"
            id="com.codeandme.custombuilder.removeBuilder"
            name="Remove Custom Builder">
      </command>

At the same time we can add some additional dependencies:
  • org.eclipse.core.commands
  • org.eclipse.jface
  • org.eclipse.ui
Now implement the commands:

package com.codeandme.custombuilder.commands;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.IHandler;
import org.eclipse.core.resources.ICommand;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.ui.handlers.HandlerUtil;

import com.codeandme.custombuilder.builders.MyBuilder;

public class AddBuilder extends AbstractHandler implements IHandler {

 @Override
 public Object execute(final ExecutionEvent event) {
  final IProject project = getProject(event);

  if (project != null) {
   try {
    // verify already registered builders
    if (hasBuilder(project))
     // already enabled
     return null;

    // add builder to project properties
    IProjectDescription description = project.getDescription();
    final ICommand buildCommand = description.newCommand();
    buildCommand.setBuilderName(MyBuilder.BUILDER_ID);

    final List<ICommand> commands = new ArrayList<ICommand>();
    commands.addAll(Arrays.asList(description.getBuildSpec()));
    commands.add(buildCommand);

    description.setBuildSpec(commands.toArray(new ICommand[commands.size()]));
    project.setDescription(description, null);

   } catch (final CoreException e) {
    // TODO could not read/write project description
    e.printStackTrace();
   }
  }

  return null;
 }

 public static IProject getProject(final ExecutionEvent event) {
  final ISelection selection = HandlerUtil.getCurrentSelection(event);
  if (selection instanceof IStructuredSelection) {
   final Object element = ((IStructuredSelection) selection).getFirstElement();

   return (IProject) Platform.getAdapterManager().getAdapter(element, IProject.class);
  }

  return null;
 }

 public static final boolean hasBuilder(final IProject project) {
  try {
   for (final ICommand buildSpec : project.getDescription().getBuildSpec()) {
    if (MyBuilder.BUILDER_ID.equals(buildSpec.getBuilderName()))
     return true;
   }
  } catch (final CoreException e) {
  }

  return false;
 }
}

package com.codeandme.custombuilder.commands;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IHandler;
import org.eclipse.core.resources.ICommand;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.runtime.CoreException;

import com.codeandme.custombuilder.builders.MyBuilder;

public class RemoveBuilder extends AbstractHandler implements IHandler {

 @Override
 public Object execute(final ExecutionEvent event) throws ExecutionException {
  final IProject project = AddBuilder.getProject(event);

  if (project != null) {
   try {
    final IProjectDescription description = project.getDescription();
    final List<ICommand> commands = new ArrayList<ICommand>();
    commands.addAll(Arrays.asList(description.getBuildSpec()));

    for (final ICommand buildSpec : description.getBuildSpec()) {
     if (MyBuilder.BUILDER_ID.equals(buildSpec.getBuilderName())) {
      // remove builder
      commands.remove(buildSpec);
     }
    }

    description.setBuildSpec(commands.toArray(new ICommand[commands.size()]));
    project.setDescription(description, null);
   } catch (final CoreException e) {
    // TODO could not read/write project description
    e.printStackTrace();
   }
  }

  return null;
 }
}

When retrieving the selected project we need to use the AdapterManager as some project types do not directly implement IProject (that is, if I remember correctly). Then we parse the build specification to add or remove our custom builder.

To add those commands to the Configure context menu we create a new menu contribution for popup:org.eclipse.ui.projectConfigure?after=additions

   <extension
         point="org.eclipse.ui.menus">
      <menuContribution
            allPopups="false"
            locationURI="popup:org.eclipse.ui.projectConfigure?after=additions">
         <command
               commandId="com.codeandme.custombuilder.addBuilder"
               style="push">
         </command>
         <command
               commandId="com.codeandme.custombuilder.removeBuilder"
               style="push">
         </command>
      </menuContribution>
   </extension>
Now you should be able to add and remove your builder.

Step 3: Selectively activate context menu entries

Only one of the commands makes sense regarding the current builder settings of a project. To enrich the user experience we will hide the invalid one.

Therefore we need to use a PropertyTester and some visibleWhen expressions as we did before in Property testers and Expression examples.

Create a new propertyTesters extension.

   <extension
         point="org.eclipse.core.expressions.propertyTesters">
      <propertyTester
            class="com.codeandme.custombuilder.propertytester.TestBuilderEnabled"
            id="com.codeandme.custombuilder.myBuilderTester"
            namespace="com.codeandme.custombuilder"
            properties="isEnabled"
            type="java.lang.Object">
      </propertyTester>
   </extension>

We leave the type to java.lang.Object as not all project types use a common base class (except Object of course). Implementing the property tester is straight forward:

package com.codeandme.custombuilder.propertytester;

import org.eclipse.core.expressions.PropertyTester;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.Platform;

import com.codeandme.custombuilder.commands.AddBuilder;

public class TestBuilderEnabled extends PropertyTester {

 private static final String IS_ENABLED = "isEnabled";

 @Override
 public boolean test(final Object receiver, final String property, final Object[] args, final Object expectedValue) {

  if (IS_ENABLED.equals(property)) {
   final IProject project = (IProject) Platform.getAdapterManager().getAdapter(receiver, IProject.class);

   if (project != null)
    return AddBuilder.hasBuilder(project);
  }

  return false;
 }
}

Now add some visibleWhen expressions to your menu entries. View the final version of the plugin.xml online.