Chapter 18. Builders and Natures

We want to extend the process of building a Java project to include running tests. If there was only one way to build a project, building would simply be represented by a method on IProject. However, the build process is complex and differs from project-to-project. In this chapter, we'll see how to contribute to the build process. In particular, we will see how to

  • Use natures to configure builders

  • Use builders to participate in the build process

  • Declare natures and builders

Each project has associated with it a set of builders. Each builder is informed any time Eclipse decides to build a project. A builder can transform resources to bring them up to date or it can validate that the resources are in a consistent state. Projects can be built explicitly, by choosing Project > Rebuild Project, or by enabling auto-build. You can turn the auto-build behavior on or off by choosing Preferences > Workbench > Perform build automatically on resource modification.

A complicated builder like the one that runs the Java compiler carefully examines the details of the changes to minimize the amount of recompilation. We don't need to be nearly that clever yet. We will just run all the tests after any change.

Resource Listeners Versus Builders

You can register an IResourceChangeListener with the workspace. Your listener will be informed whenever a resource changes (files added and deleted, file contents changed, marker changes). The changes are communicated in the form of a resource delta, a tree describing the difference between the state at the previous notification and the current state.

A builder is attached to a project. When the project is built, the builder is notified, also with a resource delta, but builders receive a delta only describing changes since the last build.

There are several distinctions between resource listeners and builders:

  • Heartbeat—Resource change listeners are run every time a resource changes. Builders are only run when the project builds. If auto-build is turned on, a listener and a builder will see a similar stream of deltas.

  • Builder order dependency—Resource listeners are invoked in an unspecified order. Builders are explicitly ordered.

  • Project order dependency—When building the workspace, projects are built based on their prerequisite relationships. You can see the dependencies on the Project References page of the project properties. Dependent projects are guaranteed to be built after their referenced projects. This order can be modified by the Build Order global preference page.

  • Granularity—With resource listeners, there is no notion of “change all the resources in the system” like there is with Build All, which will invoke all builders for all projects.

  • Independence—Resource change listeners should not depend on each other. Builders, because they are called in a defined order, can depend on each other. For example, you might generate HTML with one builder and check link consistency with another.

  • Sharing—Resource listeners are dynamically created. Builders are permanently attached to projects, stored in the repository, and shared by everyone using the project (builders are stored in the .project file, the serialized form of the ProjectDescription).

  • Scope—Resource listeners are notified by changes anywhere in the workspace. A builder is only notified about changes to resources within its project.

You can see the registered builders by looking at the External Tools Builders property page. On this page you can add additional builders that should be run. The builders registered programmatically are shown on this page as well. Figure 18.1 shows the builders for a plug-in project, as seen in Eclipse.

Scope—

Because this is a PDE plug-in project, it has additional builders for validating plug-in manifests. The plug-in manifest builder is an example of a builder that has validation as its primary purpose (see Figure 18.1).

The same builders are represented in Eclipse as an array of builder ids contained in the project's ProjectDescription. A ProjectDescription stores the metadata for a project, as shown in Figure 18.2.

ProjectDescription

Figure 18.2. ProjectDescription

Here are the levels involved in describing a project's builders:

  • Project—. Represents an Eclipse project internally

  • ProjectDescription—. Stores information about the project, like how the project is built and how this project depends on other projects

  • ICommand/BuildCommand—. A record that describes build arguments

A nature configures the capabilities of a project. A common use of natures is to install and uninstall builders. Eclipse takes care of managing natures during the life cycle of a project.

The natures are just represented by strings in the description. Note that a project can have multiple natures as well as multiple builders. For instance, plug-in projects have the Java nature and the plug-in nature. The Java nature installs the Java builder and the plug-in nature installs the plug-in manifest builder and the schema builder.

Using Natures to Configure Builders

From Figure 18.2 we can infer that when the Java nature was configured, it installed the Java builder. We will want to create an auto-test nature that will install an auto-test builder. The test looks like this. We have our usual setup and teardown using a TestProject:

Example . org.eclipse.contribution.junit.test/BuilderTest

org.eclipse.contribution.junit.test/BuilderTest
private TestProject testProject;
protected void setUp() throws Exception {
  testProject= new TestProject();
  testProject.addJar("org.junit", "junit.jar");
}
protected void tearDown() throws Exception {
  testProject.dispose();
}

We need a home for the logic required to install a nature. It doesn't seem to fit smoothly into any of the existing classes. In cases like this, where we know we need a well-known entry point, the plug-in class provides a convenient location. How can we write a test that will specify the behavior of this method before we try to implement it?

Example . org.eclipse.contribution.junit.test/BuilderTest

org.eclipse.contribution.junit.test/BuilderTest
public void testNatureAddAndRemove() throws CoreException {
  IProject project= testProject.getProject();
  JUnitPlugin.getPlugin().addAutoBuildNature(project);
  assertTrue(project.hasNature(JUnitPlugin.AUTO_TEST_NATURE));
  ICommand[] commands= project().getDescription().getBuildSpec();
  boolean found= false;
  for (int i = 0; i < commands.length; ++i)
    if (commands[i].getBuilderName().equals(
       JUnitPlugin.AUTO_TEST_BUILDER))
      found= true;
  assertTrue(found);
  JUnitPlugin.getPlugin().removeAutoBuildNature(project);
  assertFalse(project.hasNature(JUnitPlugin.AUTO_TEST_NATURE));
}

We use methods provided by the JUnitPlugin to add and remove our new nature. Notice that we pass the IProject and not the IJavaProject, because natures are associated with all projects. The assertions state that as a result of adding and configuring an auto-test nature, we should install a new builder.

Example . org.eclipse.contribution.junit/JUnitPlugin

public void addAutoBuildNature(IProject project)
   throws CoreException {
  if (project.hasNature(AUTO_TEST_NATURE))
    return;

  IProjectDescription description = project.getDescription();
  String[] ids= description.getNatureIds();
  String[] newIds= new String[ids.length + 1];
  System.arraycopy(ids, 0, newIds, 0, ids.length);
  newIds[ids.length]= AUTO_TEST_NATURE;
  description.setNatureIds(newIds);
  project.setDescription(description, null);
}
public void removeAutoBuildNature(IProject project)
   throws CoreException {
  IProjectDescription description = project.getDescription();
  String[] ids = description.getNatureIds();
  for (int i = 0; i < ids.length; ++i) {
    if (ids[i].equals(AUTO_TEST_NATURE)) {
      String[] newIds = new String[ids.length - 1];
      System.arraycopy(ids, 0, newIds, 0, i);
      System.arraycopy(ids, i + 1, newIds, i, ids.length - i - 1);
      description.setNatureIds(newIds);
      project.setDescription(description, null);
      return;
    }
  }
}

Here we see the disadvantage of communicating via typed arrays instead of lists. We want to insert our nature on the end of the list of natures. With a List, we could just say add(). With an array, we need to allocate a larger array, copy the contents of the old array, and assign the new value.

Before we can successfully install a new nature, we need to declare it as an extension.

Example . org.eclipse.contribution.junit/plugin.xml

<extension point="org.eclipse.core.resources.natures"
  id="autoTestNature"
  name="Auto Test">
  <runtime>
    <run class="org.eclipse.contribution.junit.AutoTestNature"/>
  </runtime>
  <requires-nature id="org.eclipse.jdt.core.javanature"/>
  <builder id="org.eclipse.contribution.junit.autoTestBuilder"/>
</extension>

The nature id is plug-in relative. The nature id used in the program text must match the qualified id. We introduce constants for both the nature id and the builder id matching the values in the manifest.

Example . org.eclipse.contribution.junit/JUnitPlugin

public static final String AUTO_TEST_NATURE=
     "org.eclipse.contribution.junit.autoTestNature";
public static final String AUTO_TEST_BUILDER=
     "org.eclipse.contribution.junit.autoTestBuilder";

When a nature with this id is installed, an instance of AutoTestNature will be created. The requires-nature element declares the constraint that we can't install our nature unless the Java nature is already present. In addition, this declaration tells Eclipse to configure our nature after the Java nature. The final line tells Eclipse not to run the auto-test builder unless the auto-test nature is present.

The AutoTestNature implements IProjectNature, which declares four methods. The first two initialize the project for a nature:

Example . org.eclipse.contribution.junit/AutoTestNature

public class AutoTestNature implements IProjectNature {
  IProject project;
  public AutoTestNature() {
  }
  public IProject getProject()  {
    return project;
  }
  public void setProject(IProject project)  {
    this.project= project;
  }
}

The main effort is contained in the life-cycle methods configure() and deconfigure(), which in our case will install and uninstall the auto-test builder.

Example . org.eclipse.contribution.junit/AutoTestNature

public void configure() throws CoreException {
  IProjectDescription description= getProject().getDescription();
  ICommand[] commands= description.getBuildSpec();
  for (int i = 0; i < commands.length; ++i)
    if (commands[i].getBuilderName().equals(
       JUnitPlugin.AUTO_TEST_BUILDER))
      return;

  ICommand command= description.newCommand();
  command.setBuilderName(JUnitPlugin.AUTO_TEST_BUILDER);
  ICommand[] newCommands= new ICommand[commands.length + 1];
  System.arraycopy(commands, 0, newCommands, 0, commands.length);
  newCommands[newCommands.length-1]= command;
  description.setBuildSpec(newCommands);
  getProject().setDescription(description, null);
}

We use the same trick to install the builder that we did to install the nature. If the builder is already present, we stop. Then we copy the current list of builders into a new array and add our new builder (an instance of ICommand). We are careful here to install our builder as the last, so the programs will have been compiled before we try to run them. The method deconfigure() is similar, removing the builder from the list of build commands.

Once again, before we can successfully install the builder, we must declare it as an extension. The declaration looks a lot like the nature declaration:

Example . org.eclipse.contribution.junit/plugin.xml

<extension point="org.eclipse.core.resources.builders"
  id="autoTestBuilder"
  name="Auto Test Builder">
  <builder>
    <run class="org.eclipse.contribution.junit.AutoTestBuilder"/>
  </builder>
</extension>

The test case will now pass. Now we're ready for the real end-to-end test case—save a failing test and watch it fail immediately, invoked by the builder.

Example . org.eclipse.contribution.junit.test/BuilderTest

org.eclipse.contribution.junit.test/BuilderTest
public void testAutoTesting() throws Exception {
  IWorkspaceRunnable runnable= new IWorkspaceRunnable() {
    public void run(IProgressMonitor monitor)
       throws CoreException {
      IProject project= testProject.getProject();
      JUnitPlugin.getPlugin().addAutoBuildNature(project);
      IPackageFragment pack= testProject.createPackage("pack1");
      IType type= testProject.createType(pack, "FailTest.java",
        "public class FailTest extends junit.framework.TestCase{"+
        "public void testFailure() {fail();}}");
    }
  };
  ResourcesPlugin.getWorkspace().run(runnable, null);
  IMarker[] markers= getFailureMarkers();
  assertEquals(1, markers.length);
}

We wrapped the project creation in a IWorkspaceRunnable to reduce the number of change notifications. We call the helper getFailureMarkers() to determine the number of markers. We just copied this method from MarkerTest. The insights we gain by eliminating duplication in test code are seldom worth the effort, unlike the insights gained from refactoring model code.

Now we need our builder to run the tests. We start by extending IncrementalProjectBuilder, as specified in the extension point documentation:

Example . org.eclipse.contribution.junit/AutoTestBuilder

public class AutoTestBuilder extends IncrementalProjectBuilder {
  public AutoTestBuilder() {
  }
}

The API is build(), which takes

  • An enumeration representing the type of build—full, incremental, or automatic

  • A map containing builder arguments you find in the manifest (we didn't declare any above)

  • An IProgressMonitor, because a build is potentially a long-running operation

Our implementation of build first checks to see whether there were any build errors:

Example . org.eclipse.contribution.junit/AutoTestBuilder

public boolean hasBuildErrors() throws CoreException {
  IMarker[] markers= getProject().findMarkers(
    IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER, false,
    IResource.DEPTH_INFINITE);
  for (int i= 0; i < markers.length; i++) {
    IMarker marker= markers[i];
    if (marker.getAttribute(IMarker.SEVERITY, 0) ==
       IMarker.SEVERITY_ERROR)
      return true;
  }
  return false;
}

It makes no sense to run the tests if the code didn't compile. Also, because builders are potentially invoked often, you should stop building quickly in as many cases as possible.

Example . org.eclipse.contribution.junit/AutoTestBuilder

protected IProject[] build(int kind, Map args, IProgressMonitor pm)
   throws CoreException {
  if (hasBuildErrors())
   return null;
  IJavaProject javaProject= JavaCore.create(getProject());
  IType[] types= new TestSearcher().findAll(javaProject, pm);
  JUnitPlugin.getPlugin().run(types, javaProject);
  return null;
}

The body of the build method uses our handy search object to find the test types, then invokes them through JUnitPlugin methods. The builder contains the IProject on which it is working. However, our search and test run methods take an IJavaProject. We translate by asking JavaCore to get a project for us. Going the other way is a simple accessor, IJavaProject.getProject().

Our test runs. Before we can see the behavior in action, we have to provide a user interface for adding and removing the auto-test nature. Reviewing this chapter:

  • We declared an AutoTestNature and an AutoTestBuilder.

  • We added the AutoTestNature to a project by adding its identifier to a list of nature identifiers. When it was added, the nature got the chance to also add the AutoTestBuilder to the same project.

  • When the AutoTestBuilder was told to build(), we ran the tests in the project.

Forward Pointers

  • Passing arguments to a builder—The build specification defined with ICommand allows you to register additional arguments in the form of key-value pairs. One builder class can be used a little differently in different places.

  • Incremental build—The AutoTestBuilder is not incremental. Builders can access the list of changes since the last build (IncrementalProjectBuilder.getDelta()) to limit the work they do. The changes are stored in a resource delta. For instance, the resource delta could be used to reduce the amount of searching for tests or to run only a the subset of the tests that possibly could have changed.

  • Nature image—Natures can be presented to the user as adornments to project icons.Nature image—

  • For additional background information on natures and builders refer to the article at www.eclipse.org/articles/Article-Builders/builders.html.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset