Overview

Javadoc

The Javadoc for the API is in the org.openide.compiler package. Also related to compilation are CompilerCookie, CompilerSupport, and AbstractCompileAction.

Contents

Compiler API

The Compiler API controls the execution of compilers in the IDE. It provides ways of adding custom compilers; invoking existing compilers; associating compilers with specific files; monitoring the progress and status of compilers; and clustering compiler requests into ordered jobs.

In the text below, mention will be made of compiling versus building versus cleaning, as well as compilation depths (whether to recurse folders). While these concepts are documented on a user level in the IDE (e.g. Compile versus Build or versus Compile All), you may wish to refer to Types and Depths of Compilation below for details.

Typical Flow of Control

This flow of control assumes that the standard IDE Java compiler (according to user preferences, this might be FastJavac or internal Javac) is run by the user on a collection of Java source files.
  1. A group of source files is selected in the Explorer. The Java data objects for these source files provide CompilerCookie.Compile to indicate that they can be compiled; they have done this by attaching a CompilerSupport.Compile support at creation time.
  2. The AbstractCompileAction notices that the compile cookie is present on all the selected nodes and activates itself.
  3. The user clicks on the button to Compile in the toolbar. An implementation of AbstractCompileAction.performAction(...) is called, which collects the compiler cookie (CompilerSupport.Compile) on each selected node.
  4. A new CompilerJob is created to encapsulate the compilation of all the files. It may get a display name to be used in some messages.
  5. Each selected source file is asked to add itself to the job via the CompilerCookie, which means that CompilerSupport.addToJob(...) is called.
  6. While a compiler cookie can implement this however it wishes, the standard support does not really do the work itself. Rather, it uses the Services API to find a CompilerType associated with the data object it supports. See the Services API for details on how such associations may be made, in this case for compiler types. This extra step permits the user to configure how each file should be compiled. If the user has not specified otherwise, the Java data loader provides a default for Java source files, which is also configurable by the user.
  7. The compiler type for each file is now asked to add to the compilation job using CompilerType.prepareJob(...). If for example the compiler type represented (external) FastJavac compilation, it will implement the method by creating a new compiler object which will handle compilation of this file. In this case, the compiler will extend ExternalCompiler and will include information such as the path to the *.java file to be compiled, the path to the compiler, the format of the compiler's error messages, optimization settings, and so on. Other than the file itself, these settings are initialized based on the compiler type's own user-configured properties.
  8. After creating the compiler object, the compiler type also uses CompilerJob.add(Compiler) to insert the compiler into the specified job.
  9. Before the job is run, the compilation system must ensure that all of the compilers which have been added to the job are properly clustered. For example, if ten files are being compiled, it is much more efficient to compile them all with one command. On the other hand, if they have different compiler types, and five should be compiled with deprecations on but not the other five, then two commands must be used (in this case they may be simultaneous).
  10. First CompilationEngine.createComputationLevels(CompilerJob) is called by the compilation system to check whether any of the compilers depend on one another, i.e. whether any of the compilers in the job must be run before some others in order for the latter to work properly. This would happen during multistage compilation, but in this case there are no such dependencies so just one level (stage) is created.
  11. Second CompilationEngine.createCompilerGroups(Collection) is used to cluster compilers into groups, each of which typically corresponds to a single invocation of an external compiler, etc. Different groups (in the same level) may be run simultaneously. The method Compiler.compilerGroupKey() specifies an equivalence relation that is used to group together compilers (if they are in the same level). Each group receives one CompilerGroup object created according to Compiler.compilerGroupClass().

    The grouping code, after creating each group, also calls the implementation of CompilerGroup.add(...) to notify the group of each compiler that it should contain. The external compiler group implementation, for example, keeps track of all the files it will need to compile.

  12. Now that the compiler job has been prepared, the AbstractCompileAction calls CompilerJob.start(). This will use the compilation implementation to create a CompilerTask giving control over the running compilation task. The implementation prepares the system to compile (possibly waiting for other jobs to finish, creating separate threads to contain different groups, and so on), and ultimately calls CompilerGroup.start() on each included group to actually run the compilation.
  13. While it is running, the compiler group notifies the IDE of its progress and any errors or warnings generated by Javac using CompilerGroup.fireProgressEvent(...) and CompilerGroup.fireErrorEvent(...) (the IDE has already registered listeners on these events).
  14. Upon completion of the Javac process, the compiler group notifies the IDE by returning a status code from CompilerGroup.start(). This code is transmitted via the compilation implementation back to the user of the CompilerJob as the success status of the compiler task.
  15. The compiler group implementation may have used some means of informing the filesystems involved that new class files are present. For example, the internal Javac compiler uses the FileSystems API for all file access, so all writes may trigger refreshes in affected parts of the IDE. Standard external compilation asks the file system to check the disk for changes (e.g. new class files) produced by the external process.

Invoking a Compiler

There are several ways of invoking an existing compiler, depending on how specifically you know what it is you want to invoke. They are presented here in increasing order of specificity.

Using the compile action

AbstractCompileAction.compile(...) is the easiest way to begin compilation. You need to find the compiler cookies from somewhere, e.g. starting with data objects or nodes and using:
QueueEnumeration q = new QueueEnumeration ();
for (...) {
  DataObject dob = ...;
  // or: Node node = ...;
  Object cookie = dob.getCookie (CompilerCookie.Compile.class);
  // or: ... = node.getCookie ...
  if (cookie != null) q.put (cookie);
}
AbstactCompileAction.compile (q, "Compiling some things");

Using the compile cookie

For a little more control, you can partially mimic the steps taken by the compile action. First get the CompilerCookie.Compile (or the build or clean cookies, depending on what sort of compilation you are doing) from the proper data objects or nodes as above. In most cases the returned object will actually be a CompilerSupport.Compile (or .Build or .Clean), but you do not need to depend on this being true.

Now create a compiler job using new CompilerJob(Compiler.Depth), which requires that you specify whether the compilation should be recursive or not (if only source files and not directories are involved, just use Compiler.DEPTH_ZERO). You should probably set a non-default display name using CompilerJob.setDisplayName(String).

To prepare the job, all of the files to be compiled must be added to the job, according to their compile cookies: CompilerCookie.addToJob(...).

When the job has been populated with "compilers" (i.e. individual compile requests), you may call CompilerJob.start() to actually run it. The returned CompilerTask permits you to tell whether the compilation was successful or not (when it has finished), and if necessary to halt it prematurely.

Using compiler types

If necessary, you can explicitly retrieve the compiler type for each data object to be compiled, rather than rely on the compiler support to do this. This only works if the data object uses the compiler type system, which is not necessarily the case.

If you know that the data object does do this, you may find the proper compiler type for a data object using CompilerSupport.getCompilerType(MultiDataObject.Entry) (on the primary entry) and use its CompilerType.prepareJob(...) method directly to add compilers to the job (created as above). In this case, it is necessary to specify the type of compilation here (e.g., CompilerCookie.Compile).

Using known compiler objects

The most specific method is to create the compiler job as above, but then create the compiler directly, without consulting the data object as to how it wishes to be compiled. To do so, create an instance of some implementation of Compiler; and add it to the job using CompilerJob.add(Compiler). Now start the job as above.

The IDE's standard internal Javac-based compiler implementation is not currently available in the Open APIs. However, a generic external compiler (implemented fairly simply in terms of the Execution API) is available. The standard external Javac compilation is derived from this with a few implementation enhancements. There are several constructors you may use, but typically new ExternalCompiler(FileObject,...) is most natural.

Note that in addition to the file to be compiled (which should be present in the Repository), you must configure how the classpath works, and how to recognize errors. The error expression is configured via regular expression and should be handle to handle almost all normal compiler output.

This compiler will work efficiently if multiple such compilers are added to a job - all the file arguments will be collected and passed to one invocation, assuming that the compilers are all configured identically except for the file.

Creating a Compiler

It is possible to create a compiler object that can be used for some files in the system. Typically this will be used on Java source, but that need not be the case - the API does not make any Java-specific assumptions, so e.g. compiling IDL to stubs and skeletons is quite possible.

Implementing the compiler object

The first requirement is to provide an implementation of Compiler. Several methods need to be implemented.

Implementing the group object

You must implement (or reuse) a compiler group that will actually run the compilation. Subclass CompilerGroup and implement its two abstract methods (the others it should not be necessary to override).

CompilerGroup.add(...) will be called when a new compiler associated with this type of group is created. Since there is no way to extract the compilers in the group later, it is necessary to retain any required information about the compilers now - either actual references to the compiler objects, or just the essential data about which file was requested.

It is the responsibility of the compiler group to decide whether or not to batch up files together or not, and to handle this batching if so. Also note that if the compilers have specified dependencies among themselves (see below), multiple compiler groups may be created in parallel; each one has no cross-dependencies internally, so its files may be compiled in any order; and the groups will be executed sequentially, so as to make sure the dependencies are respected.

CompilerGroup.start() should actually run the compilation to completion. It should upon completion indicate whether all files were successfully compiled or not. This method will typically be called in a dedicated thread. The compiler group should handle setting up any external process (using the Execution API, e.g.); controlling output streams; etc.

During the course of the compilation, the compiler group implementation should inform its listeners (normally just the IDE's internal compiler-handling code) of interesting events relating to the compilation:

Implementing a compiler type

For any compiler implementation which might be used outside of a very specific circumstance, and even then - it is undesirable to require the direct construction of Compiler objects by a CompilerCookie implementation, since there are usually details of the compilation (such as executable paths) which the user should be permitted to configure for themselves if necessary, possibly with multiple configurations according to the project and file. Such configuration is best handled by the existing service type system.

Instead, you may provide a CompilerType implementation which handles the details of constructing the compiler, based on the data object it is supplied. (Using a data object should almost always suffice, as a properly designed data object will correspond to exactly one compilable item.) The Services API: Creating a Service Type details aspects of creating service types which are not specific to compilation, so look at this first.

CompilerType.prepareJob(...) should just extract the relevant information from the data object, create an appropriate compiler object, and insert it into the supplied compiler job.

Generally a compiler type, like any service type, will have some user-configurable Bean properties with getters and setters and associated BeanInfo, which is all covered by the Services API. Generally the compiler type will create the proper compiler by passing it constructor arguments specifying the data object (or file object, etc.) to compile; the cookie type if needed (for example because the compiler may implement isUpToDate differently for build cookies than for compile cookies); and whatever compiler type parameters are to be used during the compilation, such as process paths, classpath information, and so on.

The second argument to prepareJob will be a type of task, such as CompilerCookie.Compile, and the method can just ignore the request if the compilation type is inappropriate. Remember to check for exact equality of the cookie class you expect to receive with the one supplied. Often clean cookies (if supported) will be handled by an entirely separate compiler and compiler group than the regular compilation, for clarity of code; a typical prepareJob implementation will switch based on the cookie as follows:

  1. CompilerCookie.Compile cookies can be handled by just creating a regular compiler and adding it to the job.
  2. CompilerCookie.Clean cookies can be handled by creating a cleaning compiler and adding it to the job.
  3. CompilerCookie.Build cookies can be handled most easily by creating both types of compilers and adding both to the job. Then use Compiler.dependsOn(Compilable) to specify that the regular compiler depends on the clean compiler. That is, the clean compiler will always be run to completion first, removing all class files associated with the object (or whatever your implementation of cleaning does); then the regular compiler will be run in a later compilation level, as desired.

Associating a compiler type with data objects

The compiler type is usually the entry point for the compiler and compiler group implementations, which is to say that it is the user-visible object which manages the configuration of the whole compilation process on a per-file basis. If you have written a compiler which is purely optional for the user, i.e. a potential feature which should be available, then no special installation code is required for it beyond a manifest entry as detailed in Services API: Installation. The user will be able to manually create and configure instances of your compiler type and then associate them with any files (or with the whole project) as needed.

If there is reason to make the new compiler type the default for some class of files, there are a couple options:

  1. If you are also creating a data loader (object type), and the data objects will use CompilerSupport, you may specify that your compiler type should be the default for this type of object by overriding CompilerSupport.defaultCompilerType().
  2. If you are not creating a new object type, but rather wish for the compiler type to be used for a specific style of development object (e.g. category of Java class), you may provide templates containing skeletal code (or other data) of your choice, which already have the compiler type set on them. Then any objects created from such a template will default to using your compiler type. To set the compiler type on the template, make sure your compiler type is installed in your copy of the IDE, associate it with the template file, and then include the filesystem.attributes file in your template JAR.

Making an external compiler

If your compiler is implemented in an unusual fashion, such as one which runs internally to the IDE's VM, then you will need to implement it by directly extending Compiler, CompilerGroup and CompilerType. However, commonly running a compiler simply means calling an external compiler process to do the job, and cleaning up the results a little by handling its error messages. In this case, you need not implement all of the logic to control an external process yourself; the APIs already include an easily extensible compiler implementation designed for this case.

Note that the API implementation of external compilation does not include any support for clean cookies - and its build cookie support does not remove old classfiles as a preliminary step, it merely affects up-to-date checks. If you do not need to support clean cookies, you should specifically ignore them if passed to your compiler type. If you do, you must handle them separately - probably by creating a custom compiler and compiler group to handle your cleaning. The reason this is not supported automatically is that cleaning is typically more dependent on the nature of the compiler (thus what files it produces) than the regular compilation.

You will want to subclass ExternalCompilerType for its basic functionality (and BeanInfo). By default it includes Bean properties permitting you to set the path to the compiler executable, as well as a regular expression used to recognize and parse compiler error messages. Decide on what additional (or replacement) parameters the compiler will need for a full configuration (e.g. optimization flags, etc.) and make Bean properties for these, remembering to create corresponding BeanInfo entries - see the Services API for general tips.

You will also want to subclass ExternalCompiler and ExternalCompilerGroup. Generally, the compiler object will hold onto various parameters as well as specific information on the file to be compiled. Typically these are passed to the compiler in its constructor by the compiler type, and made available to the compiler group object via accessor methods.

Implementing the compiler object should not be particularly tricky, as it does not itself do much, though care must be taken as usual that compilerGroupKey, equals, and hashCode are sensibly overridden. Note that isUpToDate will certainly need to be overridden if you are not using the compiler on Java classes, since its default implementation checks for matching *.class files. For other data types, just implement this to look for the expected compiler output files matching your known source files, and also check that they are newer than the source files.

The compiler group also need not be difficult. In most cases, the implementation of start in ExternalCompilerGroup will suffice, as it handles all aspects of launching and controlling the compiler process, as well as collecting error output and the exit status. Most users of this class will only need to override ExternalCompilerGroup.createProcess(...) which permits subclasses to specify details of how the external command will be assembled. This is normally done using command templates for maximum flexibility to the user. Please see the Execution API for details on how command templates may be used, since they apply also to external execution.

It is a good idea to make sure any affected file folders are refreshed before your compilation finishes (i.e. before the start method returns). The implementation in ExternalCompilerGroup will automatically refresh all file folders in which source files were contained. If your compiler produces output to alternate destinations which might be mounted in the Repository, try to refresh all potentially affected folders: override start to call the super implementation and then perform refreshes before returning. If this is not done, subsequent compilations may be performed unnecessarily (due to inaccurate up-to-date checks), execution may perform erratically, etc.

Compiling unmounted or still-uncreated sources

ExternalCompiler provides the ability to serve as the compiler for files which are not mounted in a filesystem, and thus do not have a FileObject representation; and files which actually do not exist at the time the compiler is created, but are expected to exist by the time the compiler group's start method is called. Except in these cases, the new ExternalCompiler(FileObject,...) constructor should be used.

The first case, that of unmounted files, is potentially useful for compilations which should be run on "scratch" files outside of the normal development area. These might be temporary files such as are created e.g. as intermediates in JSP compilation. To handle these, use the new ExternalCompiler(File,...) constructor. Here you can specify a java.io.File (which need not exist at the time the constructor is called).

The second case, that of files which will have FileObject representations but simply do not exist yet, is normal during multistage compilations: for example, an IDL file may be compiled to a Java source and then a classfile in one user-visible step but two compilation stages. The first can use the regular constructor; but for the second, the Java source does not exist at the time the compiler job is being prepared, so this is impossible. Instead, use new ExternalCompiler(FileSystem,String,...) (or just new ExternalCompiler(String,...)) to specify what the expected name of the source file is, so that it can be found when it is actually required.

To produce a working multistage compilation you will need to set up compiler dependencies between the stages properly, usually done by the compiler type; see below.

Types and Depths of Compilation

Compilation is, from both a user and API perspective, divided into categories in two ways: by type, and by depth. Type refers to the type of action performed on each file, and depth controls recursion into folders.

There are three defined types of compilation, each associated with a subinterface of CompilerCookie, as well as a user-visible command, and each implying a different sort of action:

  1. Compile (CompilerCookie.Compile) to compile when needed (to bring things up-to-date). That is, sources which have never been compiled will be compiled now; and sources which have changed recently will be recompiled. Sources which were compiled after their last modification will be left untouched.
  2. Build (CompilerCookie.Build) to force recompilation (whether up-to-date or not), and take any other measures to be positive everything that can be built, is. When in doubt, implementors may make this identical to .Compile.
  3. Clean (CompilerCookie.Clean) to remove any detritus associated with the compilation process, such as temporary files.
When implementing compilers, the appropriate style should be chosen for the compilation system, or two or all three styles may be supported. Data objects should normally implement all three cookies, assuming CompilerSupport is being used, since associated compilers might possibly support all three; and the compiler type should select the correct compiler implementation based on the cookie class - e.g. the Clean cookie would probably select a completely different compiler class.

There are three depths of compilation, each associated with a constant of type Compiler.Depth. Most implementors will never need to bother with these, since the combination of CompilerSupport and CompilerType handles this detail, but for those who need to know:

  1. Compiler.DEPTH_ZERO indicates that an individual file will be compiled, but folders should be ignored. There is no user-visible action which employs this depth, though API code may make use of it if desired.
  2. Compiler.DEPTH_ONE requests that files be compiled, and the immediate contents of specified folders should be compiled as well (but not the contents of subfolders). The user action Compile (for example) uses this depth.
  3. Compiler.DEPTH_INFINITE requests that files be compiled, and all contents (immediate and recursive) of subfolders. The corresponding user actions are Compile All (and so on).

A new compiler-cookie implementation need only support a compiler depth of one. This is true e.g. of CompilerSupport (which most implementors should use if possible). The standard implementation of DataFolder provides all three types of cookies at any compiler depth; recursive compilation actions (such as are presented in the Build menu) work by recursively looking for file-based data objects under the given folder, and collecting into a compiler job any that implement the same type of cookie (Compile, etc.).

Advanced Use of Dependencies

The compilation system supports a system of cross-dependencies between compilers.

It is best to give some examples of when dependencies would be needed:

All of these examples require a similar basic mechanism of specifying dependencies, which is possible in the APIs and is typically employed by a compiler type when preparing a job. First of all, both Compiler and CompilerJob implement an interface Compilable which specifies compilers included in the objects, as well as dependencies on other compilables. Both also provide methods to add dependencies (the links are for Compiler): dependsOn(Compilable) and dependsOn(Collection<Compilable>). Dependencies between compilers and/or compiler jobs may be added at any time before a job is started, though typically it is the domain of a compiler type to arrange the dependencies.

How would these methods be used in practice?

  1. A compiler type supporting build cookies by using a pair of compilers would first add both to the job. Then it would specify that the regular compiler depends on the clean compiler.
  2. A multistage compiler would add all stages to the job. It would then specify a dependency between every consecutive pair of stages to enforce the correct order.
  3. A JAR compiler could add a number of compilers to its job, and specify dependencies on all of them from its own compiler. But, this would not generally be possible to do: each data object can support compilation in its own way, and there is no polite way in the APIs to find out which compilers an object should add to a job. This is because outside code should never assume that a data object implements a subinterface of CompilerCookie in any particular way; and CompilerCookie only specifies that the object knows how to add compilers to a job, but does not give a way of obtaining these compilers directly. Therefore, it is necessary to create a new dummy compiler job. Each data object should be asked for its cookie, and this cookie used to prepare the dummy job. Now, the JAR's own compiler can specify a dependency on the dummy compiler job. This means two things: if the job to which the JAR's own compiler was added is run, then all of these other compilers will be run as well; and that all of these other compilers must finish (and succeed, i.e. return true from CompilerGroup.start()) before the JAR's compiler may be started.

Technically, all the Compilable's are arranged into a partial order according to their dependencies (calculated in a recursively transitive way). This ensures that the proper order for the entire top-level job is respected. First, all compilers are put into a partial order, i.e. a set of levels; the order of compilers within a level is unspecified, but between any two consecutive levels at least one compiler in the latter depends on at least one in the former. After levels are computed, compilers are further clustered into groups for efficiency. The levels are run in sequence by the compilation engine, while the groups within a level may be run in parallel (i.e. in different threads).

Note that there is a distinction between CompilerJob.add(Compiler) and CompilerJob.dependsOn(Compilable) (with a Compiler argument, let us say). The first ensures that the stated compiler is included in the job, which for example means that if the job depends on some other compilable, then that other compilable will be run to completion before the stated compiler is started. dependsOn will also cause the compilable (say, compiler) to be run if the whole job is started; but the compiler is just another dependency of the whole compiler job (and thus might be run in parallel with some other dependency of the job). This is why compiler types should generally use add and not dependsOn (in case some other code adds a dependency from the job).

UML Diagrams

Main structure diagram

main UML

External compiler

external compiler UML

Compiler listeners

listener UML

Compiler support

compiler support UML

Built on December 12 2001.  |  Portions Copyright 1997-2001 Sun Microsystems, Inc. All rights reserved.