How does TemplateEngine look like?
If you are curious about the TemplateEngine, then this is how it looks like -
public interface TemplateEngine { public void generate(Writer out, Map contextObjects, String encoding, Class pluginClass) throws GeneramaException; }
This generic interface allows us to plugin various template engines. Later we will see how we can develop an implementation of TemplateEngine ourselves.
The Plugin passes in
- the configured java.io.Writer object (you may write to a String or a File etc) to which the generated code needs to be written to. The processed template output will be written to this Writer.
- the context objects (namely metadata and the access to the plugin class itself ) the template needs access to , the encoding and
- the Plugin class.
The first 2 parameters probably makes sense but why do we need the Plugin Class to be passed in? The TemplateEngine uses the fully qualified package name to derive the physical location of the template file. It also expects the template name to be the same as the Plugin class name with the specified extension.
.vm - velocity and
.jelly - jelly
Personally I'm not too sure as to why a TemplateEngine should care about where the template comes from. As far as I can see, the Engine would be better off not knowing about the Plugin class. I think the physical location of the template could be passed to the Engine (may be as a java.io.Reader/java.net.URL?) from the abstract Plugin class. The problem is that anybody wanting to write an implementation of TemplateEngine interface would probably be left wondering as to why the Plugin Class is being passed to it? (happened to me atleast when I was attempting to write a TemplateEngine implementation) There is'nt anything wrong in Generama dictating the name and the location of the template file. But the Template Engine can probably remain oblivious to it.
Overriding an existing plugin template file
There might be cases where a plugin that you are using is not fully functional. There might be some issue with the template file for eg. You might want to extract the template from the respective plugin jar file and place them somewhere in the file system and edit it. Make sure that the folder where you place the template file obeys the plugin package structure and shows up before the plugin-jar in the classpath. Otherwise the modified template will not be picked up by XDoclet. Also, XDoclet2 developers would greatly appreciate it if you could submit a patch for the same. Placing the template as per the plugin package structure might not work sometimes. For eg HibernateMappingPlugin.jelly makes use of another jelly template file through an import like this.
<j:jelly xmlns:j="jelly:core" xmlns:x="jelly:xml"> <x:doctype name="hibernate-mapping" publicId="-//Hibernate/Hibernate Mapping DTD ${plugin.DTDVersion}//EN" systemId="http://hibernate.sourceforge.net/hibernate-mapping-${plugin.DTDVersion}.dtd" trim="true" <!-- Rest OMITTED FOR CLARITY --> <!-- --> <j:import uri="/Class.jelly" inherit="true"/>
In this case, you should not be placing the Class.jelly in accordance with the HibernateMappingPlugin package structure. It should be placed at the root of the classpath instead in case you feel the need to modify Class.jelly.
Where does the generated code go?
We noted earlier that the Template Engine writes the processed template output to the Writer Object that got passed to it by the Plugin. When we wrote the CommandPlugin, we saw that the command-mapping.xml file got created and the only thing we did was to specify the file name through setFilereplace method. So how did the Plugin know that the content needs to be written to a 'file' named command-mapping.xml and not to say a string / network etc? The answer lies in the CommandPlugin constructor and Pico (it is quite difficult to keep Pico out of the scheme of things).
Generama provides org.generama.defaults.FileMapperWriter implementation by default that writes to a specified file and since the CommandPlugin constructor has a dependency on the WriterMapper, Pico satisfies the dependency by injecting the FileMapperWriter instance.
public CommandPlugin(JellyTemplateEngine jellyTemplateEngine, QDoxCapableMetadataProvider metadataProvider, WriterMapper writerMapper) throws ClassNotFoundException { super (jellyTemplateEngine, metadataProvider, writerMapper);
Velocity template language is quite easy to use ( I'm not sure if the same can be said about jelly though ). So in practice, you are not likely to replace the template engine provided by generama. But then you would'nt be reading this section if you were'nt interested in knowing how that can be done. Accordingly, we will see how we can plug-in a Template engine implementation.
Providing a custom Template Engine implementation
XDoclet does not restrict plugin writers to jelly / velocity templates. If there is a different template engine that you know of and can be plugged in, you could use the corresponding templates. For instance, Groovy ships a simple template engine that allows us to write JSP like scriplets script, and EL expressions in our template in order to generate parametrized text. Let us see how we can plug that into the Generma framework. Generama provides an abstract org.generama.AbstractTemplateEngine that has some utility methods. Custom template engines might choose to extend it if required. The following class does just that and embeds Groovy's groovy.text.SimpleTemplateEngine for template processing.
import org.generama.GeneramaException; import org.generama.AbstractTemplateEngine; import groovy.text.Template; import groovy.text.SimpleTemplateEngine; import groovy.lang.Writable; import java.io.Writer; import java.util.Map; import java.io.StringWriter; import java.io.PrintWriter; import java.net.URL; /** * @author Karthik Guru */ public class GroovyTemplateEngine extends AbstractTemplateEngine { public GroovyTemplateEngine () { } public void generate (Writer out, Map contextObjects, String encoding, Class pluginClass) throws GeneramaException { try{ URL url = getScriptURL (pluginClass,".gy"); ...(1) groovy.text.TemplateEngine engine = new SimpleTemplateEngine () ; ...( 2) Writable writable = engine.createTemplate (url).make (contextObjects); ...(3) writable.writeTo (out); ...(4) } catch (Throwable e){ throw new GeneramaException ("Exception occurred when running Groovy",e); } } }
Lets take a quick look at what the above code snippet does
(1) XDoclet expects the template file name to be same as the Plugin and also expects it to reside in the same directory as the Plugin
class file. The helper method getScriptURL (defined in AbstractTemplateEngine) provides access to the template.
(2) Instantiates Groovy's SimpleTemplateEngine class that processes the template given the metadata.
(3) Initializes the template engine with the template and binds the context objects (supplied by the plugin) to the template.
(4) Writes the processed template's output to the Writer object passed into the template engine by the abstract Plugin class.
We earlier wrote a jelly template file to generate the command-mapping.xml file.The equivalent groovy template file looks like this. Groovy collections employ internal iterators that accept closures - a block of code that executes for every object in the collection (metadata in this case) passing in the object (metadata). The rest of the syntax should be pretty familiar to someone who has written a basic JSP.
<commands> <%metadata.each{obj -> %> <command name="<%=plugin.getCommandName(obj)%>" class="<%=obj.fullyQualifiedName%>"/> <%}%> </commands>
A sample build file target to register the template engine that we just developed and a modified CommandPlugin (GroovyCommandPlugin) is shown below. The only difference between GroovyCommandPlugin and CommandPlugin is in their respective constructors. CommandPlugin that we developed initially indicated its dependency on JellyTemplateEngine. GroovyCommandPlugin depends on GroovyTemplateEngine instead.
<target name="groovy.generate"> <xdoclet> <!-- defines the file handled by xdoclet2 --> <fileset dir="src/main/java"> <include name="com/xdoclet2tutorial/command/*.java"/> </fileset> <!-- register the groovy template engine.For Pico its just another component --> <component classname="com.xdoclet2tutorial.templateengine.groovy.GroovyTemplateEngine"/> <!-- Invoke the Plugin --> <component classname="com.xdoclet2tutorial.plugin.command.GroovyCommandPlugin" destdir="${gen-src-dir}"/> </xdoclet> </target>
To get GroovyTemplateEngine to work, groovy-all-1.0-jsr-03.jar needs to be in system classpath or $ANT_HOME/lib directory. You can find Groovy here. We talked about the OutputValidator interface when discussing the abstract Generama Plugin class and as to how it lets you validate the generated file. It's probably a good time to take write one ourselves.
Custom OutputValidator interface implementation
Modifying the groovy template like this
<%metadata.each{obj -> %> <%=plugin.getCommandName(obj)%>=<%=obj.fullyQualifiedName%> <%}%>
would produce a properties file instead and it could look like this.
GetPurchaseOrder=com.xdoclet2tutorial.command.GetPurchaseOrderCommand SavePurchaseOrder=com.xdoclet2tutorial.command.SavePurchaseOrderCommand UpdatePurchaseOrder=com.xdoclet2tutorial.command.UpdatePurchaseOrderCommand
In our project we typically had 100s of command classes that implemented business logic. If by mistake, a developer names a new command implementation same as an existing one through @command.class name attribute, we will have a big problem in hand. (despite several warnings developers tend to 'cut-copy' an existing file, modify it and forget to change the tag value) The resulting properties file would now have duplicate keys. They are typically read using java.util.Properties class that retains the last read unique key-value pair and worse it does'nt even throw an exception while doing so. This problem can very well be caught earlier by the validator at the time of code generation. So lets take a quick look at one such implementation.
package com.xdoclet2tutorial.plugin.command.validator; import java.io.InputStream; import java.io.IOException; import java.net.URL; import java.util.Map; import java.util.HashMap; import java.util.Properties; import org.generama.OutputValidationError; import org.generama.OutputValidator; public class CommandMappingValidator implements OutputValidator { public void validate(final URL url) throws OutputValidationError { Properties p = new Properties(){ Map m = new HashMap(); public Object put(Object key,Object value){ if (m.containsKey(key)){ throw new OutputValidationError("Duplicate key detected in properties file ! "+ key, url); }else{ m.put(key,value); } return null; } }; try{ p.load(url.openStream()); }catch(IOException e){ throw new OutputValidationError("Error obtaining Inputstream from supplied URL ",url); } } }
package java.util; public class Properties extends Hashtable{ //.. //.. public synchronized void load(InputStream inStream) throws IOException { char[] convtBuf = new char[1024]; LineReader lr = new LineReader(inStream); int limit; int keyLen; int valueStart; char c; boolean hasSep; boolean precedingBackslash; while ((limit = lr.readLine()) >= 0) { c = 0; //... //... //our hook put(key, value); } //..code omitted for clarity //.. }
If you look at the 'load()' method of java.util.Properties.java source file that ships with JDK installation, you will notice that it does all that is needed to parse the properties file in a correct manner. We are'nt bothered about that. But the last line 'put(key,value)' provides us with a hook to fit in our logic.
The anonymous Properties class in CommandMappingValidator holds on to a map internally and checks for any duplicated key in the properties file by overriding the 'load()' method throwing OutputValidationError on detecting any duplication.
Change the @command.class name attribute of SavePurchaseOrderCommand.java file to 'UpdatePurchaseOrder' , run the build and see what happens.
The GroovyCommandPlugin.java constructor requires a slight modification to get this working. You need the CommandMappingValicator to be injected and you guessed it right, Pico does it for us if you specify the same in the build.xml
public class GroovyCommandPlugin extends QDoxPlugin { public GroovyCommandPlugin(GroovyTemplateEngine groovyTemplateEngine, QDoxCapableMetadataProvider metadataProvider, WriterMapper writerMapper, CommandMappingValidator outputValidator) throws ClassNotFoundException { super(groovyTemplateEngine, metadataProvider, writerMapper); setFilereplace("command-mapping-groovy.properties"); setMultioutput(false); // register output validator with the abstract Plugin setOutputValidator(outputValidator); new TagLibrary(metadataProvider); } //... }
and the modified build.xml snippet
<target name="groovy.generate"> <xdoclet> <!-- defines the file handled by xdoclet2 --> <fileset dir="src/main/java"> <include name="com/xdoclet2tutorial/command/*.java"/> </fileset> <!-- register the groovy template engine.For Pico its just another component --> <component classname="com.xdoclet2tutorial.templateengine.groovy.GroovyTemplateEngine"/> <!-- register CommandMappingValidator --> <component classname="com.xdoclet2tutorial.plugin.command.validator.CommandMappingValidator"/> <!—- Invoke the Plugin --> <component classname="com.xdoclet2tutorial.plugin.command.GroovyCommandPlugin" destdir="${gen-src-dir}"/> </xdoclet> </target>
This was the output when i ran the build with the above modification
D:\lab\XDoclet2\>ant groovy.generate
Buildfile: build.xml
groovy.generate:
[xdoclet] Running com.xdoclet2tutorial.plugin.command.GroovyCommandPlugin
[xdoclet]
[xdoclet] GetPurchaseOrder=com.xdoclet2tutorial.command.GetPurchaseOrderCommand
[xdoclet]
[xdoclet] UpdatePurchaseOrder=com.xdoclet2tutorial.command.SavePurchaseOrderCommand
[xdoclet]
[xdoclet] UpdatePurchaseOrder=com.xdoclet2tutorial.command.UpdatePurchaseOrderCommand
[xdoclet]
[xdoclet] File /D:/lab/XDoclet2/gen-src/command-mapping-groovy.properties did not pass validation:
Duplicate key detected in properties file ! UpdatePurchaseOrder
BUILD FAILED
D:\lab\XDoclet2\build.xml:187: org.generama.OutputValidationError: Duplicate key detected in
properties file ! UpdatePurchaseOrder
Total time: 3 seconds