Implement an IOM-Based Code Generator
See how to implement the importer and exporter interfaces of a code generator in this final of two installments
by Giuseppe Naccarato
Posted March 10, 2004
Editor's Note: This is the second of two installments on using Java to write a code generator. This first installment, "Writing a Code Generator in Java" (Java Pro Online, March 3, 2004), defines a code generator, lists its benefits, and begins providing guidelines for designing and implementing a generic code generator. Here the discussion concludes with using importer and exporter interfaces, making manual implementations, and using templates.
Now that we've looked at the Internal Object Model (IOM) for our code generator application (see the first installment of this discussion, "Writing a Code Generator in Java," Java Pro Online, March 3, 2004), let's turn to the importer. The importer has the duty to read the model as input and creates the IOM. The code generator can have different importers for different data sources. In accordance with the MDA paradigm, the importer reads a model—usually exported in XMI—representing UML and created by UML tools, such as Rational Rose or Gentleware Poseidon. Our architecture is more flexible and allows for different kinds of input—for example, proprietary models or even source code.
The importer concept is translated in Java by the Importer interface:
public interface Importer {
public void start() throws
Exception;
public IOMClass createClass(
Object data) throws Exception;
public IOMAttribute
createAttribute(Object data)
throws Exception;
public IOMOperation
createOperation(Object data)
throws Exception;
public IOMParameter
createParameter(Object data)
throws Exception;
public IOMAssociation
createAssociation(Object data)
throws Exception;
public IOMRole createRole(
Object data) throws Exception;
}
The interface contains the start() method that starts the importing process. By implementing the createXxx() methods you can create the IOM structure starting with the data as input. The implementation of the Importer interface depends on the model as input, so different models can have very different implementations. Nevertheless, the common target is creating the IOM structure.
Listing 1 shows a partial implementation of the Importer interface. It is called XMLImporter and works on a simple XML representation of UML that can be seen as a simplified XMI. See Listing 2 for an example of an XML file that is an XML representation of the class diagram shown in Figure 1. The XML structure is straightforward—it recalls UML concepts such as classes, attributes, operations, parameters, associations, and roles.
The XMLImporter class uses a SAX parser. For each relevant element found in the XML as input, it calls the proper method of the Importer interface. For example the element <class> causes the invocation of the createClass() method. That method creates a new IOMClass object, fills the name and stereotype attributes with the value coming from the XML, and pushes the object into a stack for future uses. The other classXxx() methods can be implemented easily in a similar way.
The exporter translates the IOM into a specific output. In Listing 3 you can see the Exporter interface. The architecture is closely bound to the SAX parser. The start() method begins to navigate through the object model and calls the startClass() method each time a IOMClass object is processed, and in general the startXxx() method each time an instance of IOMXxx is processed. The exporter then invokes the endXxx() methods when an instance of IOMXxx is finished to be processed.
By implementing the Exporter interface, you can generate whatever kind of output you want. For example, you can see the implementation of the startClass() method generating Java source code:
public void startClass(
IOMClass cl) throws Exception {
String fileName = cl.getName() +
".java";
out = new PrintStream(
new FileOutputStream(
fileName));
out.println(
"import java.util.*;");
out.println("");
String declaration =
"public class ";
out.println(declaration +
cl.getName() + " {");
out.println();
}
It creates an output file using the name of the class and adding the JAVA extension. Then it writes, by using the information coming from the IOMClass object, Java code for declaring a class. By implementing all the methods for that interface you can obtain a JavaExporter class that creates Java classes respecting the original model in terms of attributes, operations, and associations.
Not Enough Exporters
The main class of the code generator application has three command-line parameters: the input file, an alias for the importer implementation, and an alias for the exporter implementation (see Listing 4). Launching the application in this way:
java com.ftp.codegenerator.
Generator order.xml XML Java
creates two Java classes: Order.java (see Listing 5) and Customer.java, per the information pointed out in order.xml.
Apart from very simple cases, just one type of output is not enough in a real-world system. Applications consist of a number of modules: user interface, interface layer, implementation layer, persistence, documentation, descriptors, and scripts. If your goal is to generate the code for all of these modules, then a single exporter is not enough. According to the proposed solution, you may implement different exporters and reuse the code generator multiple times, each time passing the alias of a different exporter. This works, but it is a manual operation, and if you forgot to generate a module the synchronization among them might be lost.
A simple pattern can solve the problem in a very elegant and effective way. Basically, you can implement a class ExporterGroup that contains different exporters and that is an exporter itself (see Figure 2). Each method of ExporterGroup invokes the same method of each subexporter. Consequently, when you invoke the start() method on an instance of ExporterGroup, the start() method of each exporter inside the group will be invoked as well and different kinds of output will be generated at the same time.
Unfortunately (or maybe not), not all the code can be generated. For example, let's consider the getTotalAmount() method for the Order class (see the order.xml file in the source code download). It has a boolean value (calcuateVAT) as input, which says whether the VAT has to be calculated or not. When the generator creates the related Java code, the Order.java source file does not contain the implementation of the method, but something like this:
public double getTotalAmount
(boolean calculateVAT) {
//...method implementation...
return 0.0;
}
The programmer has to write the method implementation. But there is a problem; when the code will be generated again that implementation will be overwritten. I can suggest two solutions to avoid this situation. First, the exporter can create—by using special comments—safe zones in the source code so that when the file already exists, it does not override those safe zones.
The second alternative (if the target is an object-oriented language) is that the exporter can create interface and abstract classes containing only the signature of the operations. Therefore, the programmer is forced to implement methods in specific classes extending the generated ones. In this way, it is always safe to generate code because the method implementation resides in files not involved in the generation process.
Using Templates
The implemented JavaExporter contains hard-coded, output Java code. This is not a brilliant strategy. In fact, the output code can be confused easily with the code of the generator. Yet, every time you want to change the output-generated code, you have to touch the exporter source code and compile it again. To avoid having to do this, you can use templates as the final method for generating the code. Of course, a template is not compilable code, but just a model where relevant tags can be replaced to make it compilable.
To introduce templates you need to add a template engine (Apache Velocity is a very good one for Java-based applications) and rethink a bit the exporter strategy. The template engine is able to create output code starting from an input model and a template. Although when using Apache Velocity the model could be the IOM itself, it is usually not a good strategy to allow the template engine to use the full internal model because, in this way, the logic code for generating the output would move from the generator to the template. We want to avoid this movement because having the logic code centralized in the generator will ensure that all of the templates interpret the IOM in the same way.
A reasonable strategy is to translate the IOM in a second, target-oriented model, which the template engine can use. It would have all of the data broken out into the fields required for the templates and be passed to the templates, which would do little more than placement and formatting. This is what we call a hybrid model, and it is better than the hard-coded model because you don't have the formatting portion embedded as print statements in the generator code. This hybrid model is better than templates alone because an imperative language like Java is better for doing complex model transformations and validations.
The flexible Java code generation system presented here demonstrates both its design and implementation. In addition, a simple example of a code generator was implemented to demonstrate these concepts.
About the Author
Giuseppe Naccarato has a degree in computer science and works as software developer for an IT company based in Glasgow (UK). His main interests are J2EE- and .Net-related technologies. Contact Giuseppe at .
|