14.1. BuildersA 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 builderThe 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.
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.
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.
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. IncrementalProjectBuilderThe 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.
The kind argument can have one of the following several values.
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.
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 resourcesDerived 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.
14.1.4. Associating a builder with a projectUsing 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 buildersNormally, 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
|