Documente Academic
Documente Profesional
Documente Cultură
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
Source Code
To run the example or view the source for code for this article you can unzip org.eclipse.emf.examples.jet.article2_2.0.0.zip into your plugins/ subdirectory. To use the example plug-in, you will need EMF plug-in version 2.0 installed.
Introduction
Translation vs. Generation An aspect of JET templates that is at first confusing is that generating text takes two steps: translation and generation. The first step is translating the template to a template implementation class. The second step is using this template implementation class to generate the text. If your goal with JET is to generate Java source code, it can be confusing that the template translation step also results in Java source code. Remember that this source code is not the generated text. The source code that is the result of the translation step is simply another form of the template. If you have used JSP and servlets before, you can think of a JET template as being equivalent to a JSP page. A JET template is translated to a template implementation class, just like a JSP page is translated to a servlet. The second step, where the template implementation class generates text, is equivalent to the servlet creating and returning HTML.
Part 1 of this tutorial introduced JET templates and explained how you can convert a project to a JET project to have the JET Builder automatically translate templates in your project to template implementation classes. In part 2 of this tutorial, we will focus on writing a plug-in that uses the classes in the JET package to generate Java source code. A plug-in that generates text from a JET template can no longer rely on the JET Nature and
1 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
JET Builder to automatically translate templates. This is because JET Nature and JET Builder operate only on workspace projects, not on plug-ins. Plug-ins need to use the classes in the JET package to translate their templates. The next section will discuss some of the classes in the org.eclipse.emf.codegen package. We will see what the steps are to generate source code with JET, and how the JET engine classes fit in. If you are anxious to see some code that shows how to use these classes in practice, you can go straight to A Plug-in that Generates Source Code.
2 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
will perform the following steps: 1. 2. 3. 4. 5. Create a project called .JETEmitters in the workspace Prepare this project by giving it the Java Nature and adding classpath variables to its classpath Translate the template to a template implementation Java source file in the .JETEmitters project Build the project to compile the template implementation source code to a Java .class file Call the generate method on the translated Java template implementation class and return the generated text as a String
* .JETEmitters is the default name for the project that is created during the template translation. This value can be changed by the setProjectName method. Our example plug-in will use JETEmitter and save the generated text to a Java source file in the workspace. The figure below shows the steps for generating source code using JETEmitter.
JETEmitter Gotchas
The JETEmitter class combines template translation and text generation into a single step, which makes it a very convenient tool. However, it is important that you know what takes place under the hood, otherwise you might be in for some nasty surprises. This section highlights some "gotchas" that I ran into, so that you don't make the same mistakes. 1. Plug-in Initialization Required It is not easy to use JET outside of Eclipse. JET is designed to run only as a workspace application. Any application using JET must minimally run as an Eclipse "headless" application so that plug-in initialization takes place. (The term headless refers to running Eclipse without the user interface.) This means that using JETEmitter from a simple standalone application (a standard Java class with a main method) will not work: // This fails: cannot use JETEmitter from a standalone application public static void main(String[] args) { JETEmitter emitter = new JETEmitter("/myproject/templates/HelloWorld.txtjet"); // this will throw a NullPointerException String result = emitter.generate(new NullProgressMonitor(), {"hi" }); System.out.println(result); Note that this is not a restriction of just the JETEmitter class, many of the classes in the org.eclipse.emf.codegen plug-in have dependencies on other plug-ins. The Appendix section below has more details on using JET from standalone applications. In the rest of this article we will assume that our code is running from inside a plug-in. 2. Classloader Issues You may get a NoClassDefFoundError when you pass a custom object as the argument to the JETEmitter.generate method. This can happen if the object you pass as the argument is not one of the java "bootstrap" classes (the bootstrap classes are the runtime classes in rt.jar and internationalization classes in i18n.jar). To prevent this error you must specify the classloader of your plug-in when using JETEmitter. If no classloader is specified, JETEmitter uses the classloader of its own class, which is usually the classloader for the org.eclipse.emf.codegen plug-in, and this classloader can't see much. In recent versions of EMF (since version 1.1.0 build 20030527_0913VL), JETEmitter has a constructor that takes a classloader argument.
3 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
Note that another way to specify a classloader is to subclass JETEmitter in your own project; if no classloader is specified, JETEmitter will use the classloader of this subclass. (If you are using an older version of EMF, there are no constructors that take a classloader argument and you will have no choice but to subclass JETEmitter in your own project.) The example below shows an action class that translates and invokes a selected template using JETEmitter. The example shows how a JETEmitter can be constructed with a classloader parameter or by constructing an anonymous subclass. package org.eclipse.emf.examples.jet.article2.actionexample; // imports omitted public class EmitAction implements IActionDelegate { protected ISelection selection; public void selectionChanged(IAction action, ISelection selection) { this.selection = selection; action.setEnabled(true); } public void run(IAction action) { List files = (selection instanceof IStructuredSelection) ? ((IStructuredSelection) selection).toList() : Collections.EMPTY_LIST; for (Iterator i = files.iterator(); i.hasNext();) { IFile file = (IFile) i.next(); IPath fullPath = file.getFullPath(); String templateURI = "platform:/resource" + fullPath; ClassLoader classloader = getClass().getClassLoader(); JETEmitter emitter = new JETEmitter(templateURI, classloader); // or: use an anonymous subclass // emitter = new JETEmitter(templateURI) {}; // notice the brackets try { IProgressMonitor monitor = new NullProgressMonitor(); String[] arguments = new String[] { "hi" }; String result = emitter.generate(monitor, arguments); saveGenerated(result, file); } catch (Exception e) { throw new RuntimeException(e); } } } // saveGenerated method omitted } 3. Classpath Issues JETEmitter translates your templates to Java source files in the .JETEmitters project, and invokes the JavaBuilder to compile these source files. If your templates use classes that are not standard Java classes, or not in the EMF plug-in, you will need to add these classes to the classpath of the .JETEmitters project, or the JavaBuilder cannot compile the template implementation source files. Fortunately, JETEmitter provides a simple way to do this through the method addVariable which adds a Classpath Variable to the .JETEmiter project. A Classpath Variable is a workspace-wide name that is used in Eclipse to refer to a jar file or directory. The list of all such variables can be seen using the Window > Preferences > Java > Classpath Variables menua action.. Your program will need to add a classpath variable for each jar file or directory that is needed on the classpath of the .JETEmiter project.
4 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
In this part of the JET Tutorial, we will write an Eclipse plug-in that uses a JET template to generate Java source code for typesafe enumerations. Our plug-in has to perform the following tasks: 1. Collect user input values for the variables in our template: the class name, the type and name of the attributes of the typesafe enumeration class, and the values of these attributes for each instance. We will write a simple GUI to collect these values. 2. Translate the JET template file to a Java template implementation class 3. Invoke the template implementation class with an object that contains the user input values collected by the GUI 4. Save the resulting generated source code to a location obtained from the GUI In the following sections we will go through the steps above one by one.
Typesafe Enumerations
Let's have a look at a typesafe enumeration class to see what kind of source code we want to generate. The Digit class below is an example typesafe enum. // an example typesafe enum package x.y.z; public class Digit { public static final Digit public static final Digit public static final Digit public static final Digit // ... public static final Digit
ZERO = new Digit(0, "zero"); ONE = new Digit(1, "one"); TWO = new Digit(2, "two"); THREE = new Digit(3, "three"); NINE = new Digit(9, "nine");
private static final Digit[] ALL = {ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE};
private final int value; private final String name; private Digit(int value, String name) { this.value = value; this.name = name; } public static Digit lookup(int key) { for (int i = 0; i < ALL.length; i++) { if (key == ALL[i].getValue()) { return ALL[i]; } } // lookup failed: // we have no default Digit, so we throw an exception throw new IllegalArgumentException("No digit exists for " + key); } public int getValue() { return value; } public int getName() { return name; } public String toString() { return getName(); } } Let's take a closer look at this class. First of all, the Digit class has several instances - the constants ZERO, ONE, TWO, etc. Each instance is defined by its Java variable name, "ZERO", "ONE", "TWO"..., and the values for each attribute of the enumeration class. Most typesafe enums have one or more attributes. The Digit class has two attributes: a value integer and a name String. Our example Digit class also has a lookup method, which returns the instance whose value attribute equals the specified int parameter. A lookup method introduces the concept of key attributes. Many typesafe enums have one or more attributes that uniquely distinguish one instance from another. Note that key attributes are not required: the Java VM guarantees that every newly constructed object is unique, so it is possible to have typesafe enumerations that have no attributes at all, and simply distinguish their instances
5 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
with the == instance identity operator. This works fine, but often it is convenient to have a key attribute that uniquely identifies an instance, and a lookup method that finds an instance for a specified key value. Our template does have a lookup method, so we need to decide what to do if no instance is found for the specified key value. Basically there are three options: throwing an Exception, returning a designated "default" instance, or returning null. Which option is best depends on the application in which the class is used, so we should probably let the user decide. Now that we've studied typesafe enums in more detail, let's summarize what is customizable in a typesafe enumeration: package name class name attributes, where each attribute has a type has a name may be a key attribute instances, where each instance has a name has a value for every attribute may be the default instance to return for failed lookups
6 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
<%
// instance definition public static final <%=enum.getClassName()%> <%=instance.getName()%> = new <%=enum.getClassName()%>(<%=instance.constructorValues()%>); <% } %> <% for (Iterator i = enum.attributes(); i.hasNext(); ) { %> <% Attribute attribute = (Attribute) i.next(); %> // attribute declaration private final <%=attribute.getType()%> m<%=attribute.getCappedName()%>; <% } %> /** * Private constructor. */ private <%=enum.getClassName()%>(<%=enum.constructorParameterDescription()%>) { <% for (Iterator i = enum.attributes(); i.hasNext(); ) { %> <% Attribute attribute = (Attribute) i.next(); %> m<%=attribute.getCappedName()%> = <%=attribute.getUncappedName()%>; <% } %> } // getter accessor methods <% for (Iterator i = enum.attributes(); i.hasNext(); ) { %> <% Attribute attribute = (Attribute) i.next(); %> /** * Returns the <%=attribute.getName()%>. * * @return the <%=attribute.getName()%>. */ public <%=attribute.getType()%> get<%=attribute.getCappedName()%>() { return m<%=attribute.getCappedName()%>; } <% } %> // lookup method omitted... } As you can see, the template calls some methods that were not in the simple model we introduced earlier. We Attribute.getCappedName() and have added a few convenience methods, like the getUncappedName() methods. Such methods help to keep the template simple. Another example of methods we added to the model are the TypesafeEnum.constructorParameterDescription() method and the Instance.constructorValues() method. The implementation of the constructorValues method is shown below. // class Instance /** * Convenience method that returns the attribute values of this instance, * in the order expected by the constructor of this instance. * * @return a comma-separated list of all attribute values of this instance, * formatted like attrib1-value, attrib2-value (, ...) */ public String constructorValues() { StringBuffer result = new StringBuffer(); for (Iterator i = getType().attributes(); i.hasNext(); ) { Attribute attribute = (Attribute) i.next(); result.append(getAttributeValue(attribute)); if (i.hasNext()) { result.append(", "); }
7 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
} return result.toString(); } The constructorValues method loops through the attributes of the typesafe enum, looks up the value for each attribute in the instance, and concatenates these values into a string, separated by commas. For example, in our Digit typesafe enum class above, this method would return "0, \"zero\"" for the "ZERO" instance. We could have looped through the attribute values in the template, but that would have made the template much more difficult to read. Pushing this logic into the model made the template more readable and easier to maintain. On the other hand, we lost some flexibility because users cannot customize that logic anymore by editing the template. This is a trade-off you have to make. Which is better depends on your template and your application.
Our second page collects information on the attributes of the typesafe enum class. Every attribute has a name and a type, and may be one of the key attributes. Our second wizard page is shown below:
8 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
Our third and last wizard page, shown below, collects information on the instances of the typesafe enum. The user inputs the instance name, and for each instance provides values for all attributes. Finally, one of the instances may be the "default" instance, which is the instance returned by the lookup method if no instance was found for the specified key attribute values.
9 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
When a user presses Finish on the wizard, the performFinish method in our wizard is called. The code below shows how we use a custom subclass of JETEmitter to add the jar file of our plug-in to the classpath of the .JETEmitters project before we call generate on the JETEmitter. The generated typesafe enum source code is saved to the location in the workspace that the user specified. // class NewTypesafeEnumCreationWizard protected void finishPage(IProgressMonitor monitor) throws InterruptedException, CoreException { String pluginId = "org.eclipse.emf.examples.jet.article2"; String base = Platform.getBundle(pluginId).getEntry("/").toString(); String relativeUri = "templates/TypeSafeEnumeration.javajet"; JETEmitter emitter = new JETEmitter(base + relativeUri, getClass().getClassLoader emitter.addClasspathVariable("JET_TUTORIAL", pluginId); TypesafeEnum model = mPage1.getTypesafeEnumModel(); IProgressMonitor sub = new SubProgressMonitor(monitor, 1); String result = emitter.generate(sub, new Object[] { model }); monitor.worked(1); IFile file = save(monitor, result.getBytes()); selectAndReveal(file); openResource(file); }
10 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
Note that we set the javatype attribute to true in the wizard extension element in the plugin.xml file. This will cause our wizard to show up as an action on the toolbar in the Java Perspective, as shown in the image below.
Conclusion
JET can be a great help for applications that need to generate text. Templates are as much of an improvement to code generation as JSP pages were to old style servlets. When using JET, you need to decide whether you want to distribute your templates with your application, or distribute only the template implementation classes. If your goal is to simplify the text generation capabilities of your application, then using JET Nature and JET Builder to automatically translate your templates is a good choice. See JET Tutorial Part 1 for details. In that case you only need to distribute the translated template implementation classes with your application, not the templates themselves. On the other hand, if it is important for your application that users have ultimate control over the generated text, you may want to distribute the template files themselves with your application. In that case, you will need to translate these templates every time you generate text. The plug-in we wrote in this article is an example of this type of application. This article explained what classes are available in the JET package to achieve this and showed how to use these classes with an Eclipse plug-in. The appendix below provides an overview of the JET API and shows how it can be used in headless or standalone applications.
Appendix
JET API Overview
Package org.eclipse.emf.codegen Class Description
11 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
CodeGen The CodeGen class can translate a JET template to Java source code and optionally merge the template implementation Java source code with an existing Java class. CodeGen can be used as an Eclipse headless application. The run method expects a String array parameter of two or three elements: the uri of the template to translate the target path where the translation result should be saved an optional JMerge control model file specifying how to merge the new translation result with the source code of an existing Java class CodeGenPlugin The plug-in class for the JET package. Package org.eclipse.emf.codegen.jet Class IJETNature Description Interface extending org.eclipse.core.resources.IProjectNature. Defines some of the properties that a JET nature has. Implemented by JETNature. Used as a filter for project property pages by the org.eclipse.emf.codegen.ui plugin. A org.eclipse.core.resources.IWorkspaceRunnable for adding the JET nature to a project in the workspace. Used by the AddJETNatureAction in the org.eclipse.emf.codegen.ui plug-in. This class extends org.eclipse.core.resources.IncrementalProjectBuilder. When its build method is invoked, it delegates to JETCompileTemplateOperation to translate all templates in the workspace project that have been changed since the previous build. Templates must be located in one of the folders specified as Template Containers in the JET Nature of the project. Responsible for a part of the template translation process. Generates strings for the character data present in the template file. Used by JETCompiler.
JETAddNatureOperation
JETBuilder
JETCharDataGenerator JETCompiler
This is the core class for template translation. This class is responsible for translating templates to the Java source code of a template implementation class. The actual translation is delegated to other classes in this package. A JETParser is used to parse the template into template elements. JETCompiler implements the JETParseEventListener interface and registers itself with the parser to be notified when the parser recognizes a template element. For every recognized template element, JETCompiler uses a JETGenerator to translate the template element to Java source code. When the template parsing is complete, JETCompiler uses a JETSkeleton to assemble the Java source code elements to a single compilation unit (a Java class). JETCompileTemplateOperation This class implements org.eclipse.core.resources.IWorkspaceRunnable so it can execute as a batch operation within the workspace. This operation takes a workspace project, one or more Template Containers and optionally a list of specific template files as constructor parameters. When its run method is invoked, it uses a JETCompiler to translate the template files in the specified workspace project folders to Java source files for template implementation classes. This operation can optionally be configured to trigger a complete build of the project when it is finished to compile the Java source files to .class files. JETConstantDataGenerator Responsible for a part of the template translation process. Extends JETCharDataGenerator to generate constant declarations for the strings with character data present in the template file. JETCoreElement Interface for core JET syntax elements (directive, expression, scriptlet and quote-escape). Used by JETParser. JETEmitter This class provides a convenient high-level API for users of this package. The generate method of this class translates a template to Java source code, compiles this source code to a template implementation class, asks the template class to generate text and finally returns the generated result.
12 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
This class creates a Java project called .JETEmitters in the workspace, translates the template into this project, and simply calls build on the .JETEmitters project to compile the source code. If translation or compilation fails, a JETException is thrown. A template implementation Java class is "executed" by calling its generate method. JETException JETExpressionGenerator Extends org.eclipse.core.runtime.CoreException, but provides more convenient constructors. Responsible for a part of the template translation process. Extends JETScriptletGenerator to translate JET expressions (<%= ... %> stuff) to Java source code. Interface for generators: classes that know how to translate part of a JET template to a Java source code element. A state object used by the JETParser to mark points in the JET character input stream, and delegate the processing of parts of the stream to other objects. This class implements IJETNature so that it can configure a workspace project with the JET Nature. When this nature is added to a project, it adds a JET Builder to the front of the build spec of the project. This nature defines two properties: Template Containers - a list of folders in the project that contain the JET templates to translate. Source Container - the target folder in which to save translated template implementation Java classes. These properties are used by the JET Builder when performing a build. JETParseEventListener JETParser Interface for objects that know how to process parts of a JET character input stream. The main parser class. Has several inner classes for recognizing core JET syntax elements (directive, expression, scriptlet and quote-escape). When a core JET syntax element is recognized, the actual processing of the element is delegated to a JETParseEventListener. An input buffer for the JET parser. Provides a stackStream method that others can call with the character stream to an include file. Also provides many other convenience methods for the parser. Responsible for a part of the template translation process. Translates JET scriptlets (<% ... %> stuff) to Java source code. This class provides an interface for assembling Java source code elements into a single Java compilation unit (a Java class). Java source code elements are assembled according to a class skeleton definition. A skeleton can be used to add boilerplate code to a translated template implementation class. This class provides a default custom template implementation class skeleton definition, but can also assemble Java elements using a custom skeleton. The actual parsing and generation of Java source code is delegated to classes in the org.eclipse.jdt.core.jdom package.
JETGenerator JETMark
JETNature
JETReader
JETScriptletGenerator JETSkeleton
Package org.eclipse.emf.codegen.jmerge Class JControlModel JMerger Description A control model that provides dictionaries and rules to drive a merge process. A class for merging Java source files. Uses classes in the org.eclipse.jdt.core.jdom package to parse the source code. This class can be used by application code, but can also be run as an Eclipse headless application. JPatternDictionary A dictionary of signatures and JDOM nodes. PropertyMerger A class for merging property files. This class can be used by application code, but can also be run as an Eclipse headless application.
13 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
The org.eclipse.emf.codegen.CodeGen class can translate a JET template to Java source code and optionally merge the template implementation Java source code with an existing Java class. CodeGen can be used as an Eclipse headless application ("headless" means that the Eclipse GUI does not start). The plugins/org.eclipse.emf.codegen/test folder in your Eclipse installation contains some scripts for launching the CodeGen class as an Eclipse headless application. These scripts are in Unix format. Below is an example script for Windows. Note that we pass two arguments to the CodeGen class: the the uri of the template to translate target path where the translation result should be saved
If the target path already contains a previous translation result, and you want to merge the new translation result with the existing one, you can specify a JMerge control model file as the third argument. The plugins/org.eclipse.emf.codegen/test folder in your Eclipse installation contains an example merge.xml file. @echo off set ECLIPSE_HOME=C:\eclipse-2.1\eclipse set WORKSPACE=%ECLIPSE_HOME%\workspace set OPTS=-Xmx900M -Djava.compiler=NONE -verify -cp %ECLIPSE_HOME%\startup.jar set MAIN=org.eclipse.core.launcher.Main -noupdate -data %WORKSPACE% set TEMPLATE_URI=test.javajet set TARGET_FOLDER=C:\temp\jetstandalone\MyProject set ARGUMENTS=%TEMPLATE_URI% %TARGET_FOLDER% echo Shut down Eclipse before running this script. java %OPTS% %MAIN% -application org.eclipse.emf.codegen.CodeGen %ARGUMENTS%
14 of 15
10/8/2007 11:33 AM
http://www.eclipse.org/articles/Article-JET2/jet_tutorial2.html
</classpath> </taskdef> <!-- Usage example 1: --> <!-- Specify the template file in the "template" attribute. --> <!-- You can use the "class" and "package" attributes to override the --> <!-- "class" and "package" attributes in the template file. --> <target name="jetc_single_template"> <mkdir dir="jet-output"/> <jetc template="test.xmljet" package="com.foo" class="Test" destdir="jet-output"/> <javac srcdir="jet-output" destdir="classes"/> </target> <!-- Usage example 2: --> <!-- Translate a bunch of template files at once. --> <!-- You cannot use the "class" and "package" attributes when using a fileset. --> <target name="jetc_multiple_templates"> <mkdir dir="jet-output"/> <jetc destdir="jet-output"> <fileset dir="jet-templates" includes="*.*jet"/> </jetc> <javac srcdir="jet-output" destdir="classes"/> </target> </project>
Resources
Substitutes for Missing C Constructs (By Joshua Bloch) Java Tip 122: Beware of Java typesafe enumerations (By Vladimir Roubtsov) Java Tip 133: More on typesafe enums (By Philip Bishop) http://www.eclipse.org/emf/
Java and all Java-based trademarks and logos are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States, other countries, or both.
15 of 15
10/8/2007 11:33 AM