Previous Page
Next Page

14.1. Builders

A builder is scoped to a project. When one or more resources in a project change, the builders associated with the project are notified. If these changes have been batched (see Section 9.3, Batching Change Events, on page 382), the builder receives a single notification containing a list of all the changed resources rather than individual notifications for each changed resource.

Tip

If you want a global builder not associated with any specific project, hook into the early startup extension point (see Section 3.4.2, Early plug-in startup, on page 114) and add a workspace resource change listener (see Section 9.1, IResourceChangeListener, on page 375). The downside of this approach is that the builder will consume memory and execution cycles regardless of whether it is really needed.


Builders process the list of changes and update their build state by regenerating the necessary derived resources (see Section 14.1.3, Derived resources, on page 509), annotating source resources, and so on. Builders are notified when a resource changes, such as when a user saves a modified Java source file, and thus are executed quite frequently. Because of this, a builder must execute incrementally, meaning that it must rebuild only those derived resources that have changed.

If the Eclipse Java compiler rebuilt all the Java source files in the project every time a single Java source file was saved, it would bring Eclipse to its knees.

14.1.1. Declaring a builder

The first step in creating the plugin.properties auditor involves adding a builder declaration to the Favorites plug-in manifest. Open the plug-in manifest editor on the Favorites plugin.xml file, switch to the Extensions page, and add an org.eclipse.core.resources.builders extension (see Figure 14-2).

Figure 14-2. The New Extension wizard showing the org.eclipse.core.resources.builders


Click on the org.eclipse.core.resources.builders extension to edit its properties, and set the id attribute for the extension (see Figure 14-3).

Figure 14-3. The plug-in manifest editor showing the builder's extension.


id "propertiesFileAuditor"

The last segment of the builder's unique identifier. If the declaration appears in the com.qualityeclipse.favorites plug-in, then the builder's fully qualified identifier is com.qualityeclipse.favorites.propertiesFileAuditor.

Right-click on the extension and select New > builder in the context menu. The builder element has these two attributes (see Figure 14-4).

Figure 14-4. The plug-in manifest editor showing the builder's attributes.


hasNature "true"

A Boolean indicating whether the builder is owned by a project nature. If true and no corresponding nature is found, this builder will not run, but will remain in the project's build spec. If the attribute is not specified, it is assumed to be false.

isConfiguarble Leave blank

A Boolean indicating whether the builder allows customization of which build triggers it will respond to. If TRue, clients will be able to use the API ICommand.setBuilding to specify whether this builder should be run for a particular build trigger. If the attribute is not specified, it is assumed to be false.

Right-click on the builder element and select New > run in the context menu to associate a Java class with the builder. The Java class will provide behavior for the builder. The run element has only one attribute (see Figure 14-5), class, specifying the Java class to be executed.

Figure 14-5. The plug-in manifest editor showing the run attributes.


Click the class: label to the right of the class field and use the Java Attribute Editor to create a new class in the Favorites project with the specified package and class name.

class "com.qualityeclipse.favorites.builder.PropertiesFileAuditor"

The fully qualified name of a subclass of org.eclipse.core.resources.IncrementalProjectBuilder. The class is instantiated using its no argument constructor but can be parameterized using the IExecutableExtension interface (see Section 20.5.1, Parameterized types, on page 724).

The complete declaration in the Favorites plug-in manifest should look like this:

<extension
   id="propertiesFileAuditor"
   point="org.eclipse.core.resources.builders">
   <builder hasNature="true">
      <run class=
      "com.qualityeclipse.favorites.builder.PropertiesFileAuditor"/>
   </builder>
</extension>

14.1.2. IncrementalProjectBuilder

The class specified in the declaration of the previous section must be a subclass of IncrementalProjectBuilder, and at the very least, should implement the build() and clean() methods. The build() method is called by Eclipse when the builder should either incrementally or fully build related files and markers. This method has several arguments providing build information and a mechanism for displaying progress to the user.

kind The kind of build being requested. Valid values include: FULL_BUILD, INCREMENTAL_BUILD, and AUTO_BUILD.

args A map of builder-specific arguments keyed by argument name (key type: String, value type: String) or null, indicating an empty map.

monitor A progress monitor, or null if progress reporting and cancellation are not desired.

The kind argument can have one of the following several values.

FULL_BUILD The builder should rebuild all derived resources and perform its work as if it has not been executed before.

CLEAN_BUILD The builder should delete all derived resources and markers before performing a full build (see the discussion of the clean() method that follows).

INCREMENTAL_BUILD The builder should only rebuild those derived resources that need to be updated and only perform the work that is necessary based on its prior build state.

AUTO_BUILD Same as INCREMENTAL_BUILD, except that the build was an automatically triggered incremental build (auto-building ON).

Calling IWorkspace.build() or IProject.build() whenever the build kind is CLEAN_BUILD TRiggers the clean() method prior to calling the build() method with the build kind equal to FULL_BUILD. The clean() method should discard any additional state that has been computed as a result of previous builds including all derived resources and all markers of type IMarker.PROBLEM. The platform will take care of discarding the builder's last built state (no need to call forgetLastBuiltState()). The following are several interesting methods in IncrementalProjectBuilder.

build(int, Map, IProgressMonitor) Overridden by subclasses to perform the build operation. See the description earlier in this section and the implementation example later in this section.

clean(IProgressMonitor) Similar to build(), except all derived resources, generated markers, and previous state should be discarded before building.

forgetLastBuiltState() Requests that this builder forget any state it may be caching regarding previously built states. This may need to be called by a subclass if the build process is interrupted or canceled (see checkCancel() method later in this section).

getCommand() Returns the build command associated with this builder which may contain project-specific configuration information (see Section 14.1.4, Associating a builder with a project, on page 509).

geTDelta(IProject) Returns the resource delta recording the changes in the given project since the last time the builder was run, or null if no such delta is available. See Section 9.2, Processing Change Events, on page 379 for details on processing resource change events and the shouldAudit() method later on in this section.

getProject() Returns the project with which this builder is associated.

isInterrupted() Returns whether an interrupt request has been made for this build. Background auto-build is interrupted when another thread tries to modify the workspace concurrently with the build thread. See shouldAudit() method later on in this section.

setInitializationData(IConfigurationElement, String, Object) Called immediately after the builder is instantiated with configuration information specified in the builder's declaration (see Section 20.5.1, Parameterized types, on page 724).

After declaring the builder in the previous section, you must implement PropertiesFileAuditor, a subclass of org.eclipse.core.resources.IncrementalProjectBuilder, to perform the operation. When the build() method is called, the PropertiesFileAuditor builder delegates to shouldAudit() to see whether an audit should be performed and, if necessary, to auditPluginManifest() to perform the audit.

package com.qualityeclipse.favorites.builder;

import ...

public class PropertiesFileAuditor
   extends IncrementalProjectBuilder
{
   protected IProject[] build(
      int kind,
      Map args,
      IProgressMonitor monitor
   ) throws CoreException
   {
      if (shouldAudit(kind)) {
         auditPluginManifest(monitor);
      }

      return null;
   }

   ... other methods discussed later inserted here ...
}

The shouldAudit() method checks for FULL_BUILD, or if the plugin.xml or plugin.properties files of a project have changed (see Section 9.2, Processing Change Events, on page 379). If a builder has never been invoked before, then getdelta() returns null.

private boolean shouldAudit(int kind) {
   if (kind == FULL_BUILD)
      return true;
   IResourceDelta delta = getDelta(getProject());
   if (delta == null)
      return false;
   IResourceDelta[] children = delta.getAffectedChildren();
   for (int i = 0; i < children.length; i++) {
      IResourceDelta child = children[i];
      String fileName = child.getProjectRelativePath().lastSegment();
      if (fileName.equals("plugin.xml")
         || fileName.equals("plugin.properties"))
         return true;
   }
   return false;
}

If the shouldAudit() method determines that the manifest and properties files should be audited, then the auditPluginManifest() method is called to scan the plugin.xml and plugin.properties files and correlate the key/value pairs; any keys appearing in plugin.xml should have a corresponding key/value pair in plugin.properties. Before each lengthy operation, check to see whether the build has been interrupted or canceled. After each lengthy operation, you report progress to the user (see Section 9.4, Progress Monitor, on page 383); while this is not strictly necessary, it is certainly polite. If you do prematurely exit your build process, you may need to call forgetLastBuildState() before exiting so that a full rebuild will be performed the next time.

public static final int MISSING_KEY_VIOLATION = 1;
public static final int UNUSED_KEY_VIOLATION = 2;

private void auditPluginManifest(IProgressMonitor monitor) {
   monitor.beginTask("Audit plugin manifest", 4);
   IProject proj = getProject();

   if (checkCancel(monitor)) {
      return;
   }
   Map pluginKeys = scanPlugin(getProject().getFile("plugin.xml"));
   monitor.worked(1);

   if (checkCancel(monitor)) {
      return;
   }
   Map propertyKeys = scanProperties(
      getProject().getFile("plugin.properties"));
   monitor.worked(1);
   if (checkCancel(monitor)) {
      return;
   }

   Iterator iter = pluginKeys.entrySet().iterator();
   while (iter.hasNext()) {
      Map.Entry entry = (Map.Entry) iter.next();
      if (!propertyKeys.containsKey(entry.getKey()))
         reportProblem(
            "Missing property key",
            ((Location) entry.getValue()),
            MISSING_KEY_VIOLATION,
            true);
   }
   monitor.worked(1);

   if (checkCancel(monitor)) {
      return;
   }

   iter = propertyKeys.entrySet().iterator();
   while (iter.hasNext()) {
      Map.Entry entry = (Map.Entry) iter.next();
      if (!pluginKeys.containsKey(entry.getKey()))
         reportProblem(
            "Unused property key",
            ((Location) entry.getValue()),
            UNUSED_KEY_VIOLATION,
            false);
   }
   monitor.done();
}

private boolean checkCancel(IProgressMonitor monitor) {
   if (monitor.isCanceled()) {
      // Discard build state if necessary.
      throw new OperationCanceledException();
   }

   if (isInterrupted()) {
      // Discard build state if necessary.
      return true;
  }
  return false;
}

The auditPluginManifest() method delegates scanning the plugin.xml and plugin.properties to two separate scan methods.

private Map scanPlugin(IFile file) {
   Map keys = new HashMap();
   String content = readFile(file);
   int start = 0;
   while (true) {
      start = content.indexOf("\"%", start);
      if (start < 0)
         break;
      int end = content.indexOf('"', start + 2);
      if (end < 0)
         break;
      Location loc = new Location();
      loc.file = file;
      loc.key = content.substring(start + 2, end);
      loc.charStart = start + 1;
      loc.charEnd = end;
      keys.put(loc.key, loc);
      start = end + 1;
   }
   return keys;
}

private Map scanProperties(IFile file) {
   Map keys = new HashMap();
   String content = readFile(file);
   int end = 0;
   while (true) {
      end = content.indexOf('=', end);
      if (end < 0)
         break;
      int start = end - 1;
      while (start >= 0) {
         char ch = content.charAt(start);
         if (ch == '\r' || ch == '\n')
            break;
         start--;
      }
      start++;
      String found = content.substring(start, end).trim();
      if (found.length() == 0
         || found.charAt(0) == '#'
         || found.indexOf('=') != -1)
         continue;
      Location loc = new Location();
      loc.file = file;
      loc.key = found;
      loc.charStart = start;
      loc.charEnd = end;
      keys.put(loc.key, loc);
      end++;
   }
   return keys;
}

The following two scan methods read the file content into memory using the readFile() method.

private String readFile(IFile file) {
   if (!file.exists())
      return "";
   InputStream stream = null;
   try {
      stream = file.getContents();
      Reader reader =
         new BufferedReader(
            new InputStreamReader(stream));
      StringBuffer result = new StringBuffer(2048);
      char[] buf = new char[2048];
      while (true) {
         int count = reader.read(buf);
         if (count < 0)
            break;
         result.append(buf, 0, count);
      }
      return result.toString();
   }
   catch (Exception e) {
      FavoritesLog.logError(e);
      return "";
   }
   finally {
      try {
         if (stream != null)
            stream.close();
      }
      catch (IOException e) {
         FavoritesLog.logError(e);
         return "";
      }
   }
}

The reportProblem() method appends a message to standard output. In subsequent sections, this method will be enhanced to generate markers instead (see Section 14.2.2, Creating and deleting markers, on page 515).

private void reportProblem(
   String msg, Location loc, int violation, boolean isError
) {
   System.out.println(
      (isError ? "ERROR: " : "WARNING: ")
         + msg + " \""
         + loc.key + "\" in "
         + loc.file.getFullPath());
}

The Location inner class is defined as an internal data holder with no associated behavior.

private class Location
{
   IFile file;
   String key;
   int charStart;
   int charEnd;
}

When hooked up to a project (see Section 14.1.4, Associating a builder with a project, on page 509 and Section 14.3.7, Associating a nature with a project, on page 532), the builder will append problems similar to the following to standard output.

ERROR: Missing property key "favorites.category.name"
   in /Test/plugin.xml
ERROR: Missing property key "favorites.view.name"
   in /Test/plugin.xml
WARNING: Unused property key "two"
   in /Test/plugin.properties
WARNING: Unused property key "three"
   in /Test/plugin.properties

14.1.3. Derived resources

Derived resources are ones that can be fully regenerated by a builder. Java class files are derived resources because the Java compiler can fully regenerate them from the associated Java source file. When a builder creates a derived resource, it should mark that file as derived using the IResource.setDerived() method. A team provider can then assume that the file does not need to be under version control by default.

setDerived(boolean) Sets whether this resource subtree is marked as derived. This operation does not result in a resource change event and does not trigger auto-builds.

14.1.4. Associating a builder with a project

Using a nature to associate a builder with a project is the preferred approach (see Section 14.3, Natures, on page 525), but you can associate builders with projects without using a nature. You could create an action in a workbench window (see Section 6.2.6, Creating an action delegate, on page 216) that calls the following addBuilderToProject() method to associate your auditor with the currently selected projects. Alternatively, you could, on startup, cycle through all the projects in the workbench and call the following addBuilderToProject() method. If you do not use a project nature, then be sure to set the hasNature attribute to false (see Figure 14-4 on page 501).

There are no advantages or disadvantages to associating a builder with a project using an action delegate as opposed to using a project nature, but in this case, you will create a project nature to make the association (see Section 14.3, Natures, on page 525). Place the following in the favorites PropertiesFileAuditor class.

public static final String BUILDER_ID =
   FavoritesPlugin.ID + ".propertiesFileAuditor";

public static void addBuilderToProject(IProject project) {

   // Cannot modify closed projects.
   if (!project.isOpen())
      return;

   // Get the description.
   IProjectDescription description;
   try {
      description = project.getDescription();
   }
   catch (CoreException e) {
      FavoritesLog.logError(e);
      return;
   }

   // Look for builder already associated.
   ICommand[] cmds = description.getBuildSpec();
   for (int j = 0; j < cmds.length; j++)
      if (cmds[j].getBuilderName().equals(BUILDER_ID))
         return;

   // Associate builder with project.
   ICommand newCmd = description.newCommand();
   newCmd.setBuilderName(BUILDER_ID);
   List newCmds = new ArrayList();
   newCmds.addAll(Arrays.asList(cmds));
   newCmds.add(newCmd);
   description.setBuildSpec(
      (ICommand[]) newCmds.toArray(
         new ICommand[newCmds.size()]));
   try {
      project.setDescription(description, null);
   }
   catch (CoreException e) {
      FavoritesLog.logError(e);
   }
}

Every workbench project contains a .project file (see Section 1.4.2, .classpath and .project files, on page 22) that contains build commands. Executing this method causes the following to appear in the buildSpec section of the project's .project file.

<buildCommand>
   <name>
      com.qualityeclipse.favorites.propertiesFileAuditor
   </name>
   <arguments>
</arguments>
</buildCommand>

In addition to the addBuilderToProject() method, you would need a corresponding removeBuilderFromProject() method:

public static void removeBuilderFromProject(IProject project) {

   // Cannot modify closed projects.
   if (!project.isOpen())
      return;

   // Get the description.
   IProjectDescription description;
   try {
      description = project.getDescription();
   }
   catch (CoreException e) {
      FavoritesLog.logError(e);
      return;
   }

   // Look for builder.
   int index = -1;
   ICommand[] cmds = description.getBuildSpec();
   for (int j = 0; j < cmds.length; j++) {
      if (cmds[j].getBuilderName().equals(BUILDER_ID)) {
         index = j;
         break;
      }
   }
   if (index == -1)
      return;

   // Remove builder from project.
   List newCmds = new ArrayList();
   newCmds.addAll(Arrays.asList(cmds));
   newCmds.remove(index);
   description.setBuildSpec(
      (ICommand[]) newCmds.toArray(
         new ICommand[newCmds.size()]));
   try {
      project.setDescription(description, null);
   }
   catch (CoreException e) {
      FavoritesLog.logError(e);
   }
}

14.1.5. Invoking builders

Normally, the build process for a project is triggered either by the user selecting a build action or by the workbench during an auto-build in response to a resource change. If need be, you can trigger the build process programmatically using one of the following methods:

IProject

build(int, IProgressMonitor) Runs the build processing on the project, causing all associated builders to be run. The first argument indicates the kind of build, FULL_BUILD, INCREMENTAL_BUILD or CLEAN_BUILD (see Section 14.1.2, IncrementalProjectBuilder, on page 502).

build(int, String, Map, IProgressMonitor) triggers a single builder to be run on the project. The first argument indicates the kind of build, FULL_BUILD, INCREMENTAL_BUILD or CLEAN_BUILD (see Section 14.1.2, IncrementalProjectBuilder, on page 502), while the second specifies which builder is to be run.

IWorkspace

build(int, IProgressMonitor) Runs the build processing on all open projects in the workspace. The first argument indicates the kind of build, FULL_BUILD, INCREMENTAL_BUILD or CLEAN_BUILD (see Section 14.1.2, IncrementalProjectBuilder, on page 502).


Previous Page
Next Page