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 googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly. 

Step 1: Creating the builder

This is the easy part. Create a new Plug-in project named com.example.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.example.custombuilder.myBuilder, leave the builder settings empty and create a run entry below. There set the class to com.example.custombuilder.MyBuilder. Implement the class with following code:

package com.example.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.example.custombuilder.myBuilder";

 @Override
 protected IProject[] build(final int kind, final Map 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.

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

<extension point="org.eclipse.ui.commands">
    <command defaultHandler="com.example.custombuilder.commands.AddBuilder" id="com.example.custombuilder.addBuilder" name="Add Custom Builder">
    </command>
    <command defaultHandler="com.example.custombuilder.commands.RemoveBuilder" id="com.example.custombuilder.removeBuilder" name="Remove Custom Builder">
    </command>
</extension>

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.example.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.example.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.example.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.example.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.example.custombuilder.addBuilder" style="push">
        </command>
        <command commandId="com.example.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.example.custombuilder.propertytester.TestBuilderEnabled"
            id="com.example.custombuilder.myBuilderTester"
            namespace="com.example.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.example.custombuilder.propertytester;

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

import com.example.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. Here is the full plugin.xml code for your reference:

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         id="com.example.custombuilder.myBuilder"
         point="org.eclipse.core.resources.builders">
      <builder>
         <run
               class="com.example.custombuilder.builders.MyBuilder">
         </run></builder>
   </extension>
   <extension
         point="org.eclipse.ui.menus">
      <menuContribution
            allPopups="false"
            locationURI="popup:org.eclipse.ui.projectConfigure?after=additions">
         <command
               commandId="com.example.custombuilder.addBuilder"
               style="push">
            <visibleWhen
                  checkEnabled="false">
               <and>
                  <count
                        value="1">
                  </count>
                  <iterate
                        ifEmpty="false"
                        operator="and">
                     <adapt
                           type="org.eclipse.core.resources.IProject">
                     </adapt>
                  </iterate>
                  <iterate>
                     <not>
                        <test
                              forcePluginActivation="true"
                              property="com.example.custombuilder.isEnabled">
                        </test>
                     </not>
                  </iterate>
               </and>
            </visibleWhen>
         </command>
         <command
               commandId="com.example.custombuilder.removeBuilder"
               style="push">
            <visibleWhen
                  checkEnabled="false">
               <and>
                  <count
                        value="1">
                  </count>
                  <iterate
                        ifEmpty="false"
                        operator="and">
                     <adapt
                           type="org.eclipse.core.resources.IProject">
                     </adapt>
                  </iterate>
                  <iterate>
                     <test
                           forcePluginActivation="true"
                           property="com.example.custombuilder.isEnabled">
                     </test>
                  </iterate>
               </and>
            </visibleWhen>
         </command>
      </menuContribution>
   </extension>
   <extension
         point="org.eclipse.ui.commands">
      <command
            defaultHandler="com.example.custombuilder.commands.AddBuilder"
            id="com.example.custombuilder.addBuilder"
            name="Add Custom Builder">
      </command>
      <command
            defaultHandler="com.example.custombuilder.commands.RemoveBuilder"
            id="com.example.custombuilder.removeBuilder"
            name="Remove Custom Builder">
      </command>
   </extension>
   <extension
         point="org.eclipse.core.expressions.propertyTesters">
      <propertyTester
            class="com.example.custombuilder.propertytester.TestBuilderEnabled"
            id="com.example.custombuilder.myBuilderTester"
            namespace="com.example.custombuilder"
            properties="isEnabled"
            type="java.lang.Object">
      </propertyTester>
   </extension>

</plugin>

1 comment:

  1. You don't need a tester for that, as the core expressions gives you access to the project nature, and natures are the only allowed way to link builders to projects:
    <visibleWhen
    checkEnabled="false">
    <with
    variable="selection">
    <count
    value="1">
    </count>
    <iterate>
    <and>
    <instanceof
    value="org.eclipse.core.resources.IProject">
    </instanceof>
    <test
    property="org.eclipse.core.resources.projectNature"
    value="your.nature">
    </test>
    </and>
    </iterate>
    </with>
    </visibleWhen>

    ReplyDelete