Code generation
In several projects at different places of work, I used code generation. What for?
Most often this is achieved by maintaining the correctness of the code with changes. For example, when adding another data type to the domain model, you need to add a class in Java, a class in C ++, a conversion code between them, and a value in Enum. Without generating code and working with pens a lot, and there is always a chance to forget something from this.
Sometimes reflection can be a substitute for generating code, but even then such code will be less readable than a template for generation.
The source information for generating code can look very different - it can be a Java class that, through reflection, reads the properties and generates a class in C ++, or a csv file, or XML, or something else.
I'll try to tell you how I generate code in maven projects using the jamon framework.
Formulation of the problem
For example, let's take a task from the service for maps, which I have been writing for a long time, but have not written yet. There is a point on the map in Jerusalem and for it a list of properties that this point can have. For example, it refers to the time of the second temple or to Byzantium, a Christian or Muslim shrine, etc.
The source file looks like this:
name,group christian, religion jewish, religion muslim, religion ancient, time firsttemple, time secondtemple, time hasmonean, time hordus, time roma, time byzantee, time arab, time ottoman, time persian, time crusaider, time pray, type museum, type restaurant, type other, type
name,group christian, religion jewish, religion muslim, religion ancient, time firsttemple, time secondtemple, time hasmonean, time hordus, time roma, time byzantee, time arab, time ottoman, time persian, time crusaider, time pray, type museum, type restaurant, type other, type
name,group christian, religion jewish, religion muslim, religion ancient, time firsttemple, time secondtemple, time hasmonean, time hordus, time roma, time byzantee, time arab, time ottoman, time persian, time crusaider, time pray, type museum, type restaurant, type other, type
|
name,group christian, religion jewish, religion muslim, religion ancient, time firsttemple, time secondtemple, time hasmonean, time hordus, time roma, time byzantee, time arab, time ottoman, time persian, time crusaider, time pray, type museum, type restaurant, type other, type
')
Based on this, I want to create a file of this type:
package org.citymap; import java.io.Serializable; public class PointAttributes implements Serializable { private boolean muslim; public boolean isMuslim () { return muslim; } public void setMuslim ( boolean value ) { muslim = value; } .... public boolean equals ( Object o ) { if ( this == o ) return true ; if ( o == null || getClass () != o.getClass ()) return false ; PointAttributes that = ( PointAttributes ) o; if ( muslim != that.muslim ) return false ; if ( christian != that.christian ) return false ; ... return true ; } public int hashCode () { int result = 0 ; result = 31 * result + ( muslim ? 1 : 0 ) ; result = 31 * result + ( christian ? 1 : 0 ) ; .... return result; } }
|
package org.citymap;
import java.io.Serializable;
public class PointAttributes implements Serializable {
private boolean muslim;
public boolean isMuslim () {
return muslim;
}
public void setMuslim ( boolean value ) {
muslim = value;
}
....
public boolean equals ( Object o ) {
if ( this == o ) return true ;
if ( o == null || getClass () != o.getClass ()) return false ;
PointAttributes that = ( PointAttributes ) o;
if ( muslim != that.muslim ) return false ;
if ( christian != that.christian ) return false ;
...
return true ;
}
public int hashCode () {
int result = 0 ;
result = 31 * result + ( muslim ? 1 : 0 ) ;
result = 31 * result + ( christian ? 1 : 0 ) ;
....
return result;
}
}
Maven
We create the project in maven. We make it multi-modular, otherwise it is impossible to generate beautiful code. In the module dad listed submodules:
<modules> <module>util</module> <module>codegen</module> <module>common</module> <module>persistency</module> <module>web</module> </modules>
<modules> <module>util</module> <module>codegen</module> <module>common</module> <module>persistency</module> <module>web</module> </modules>
<modules> <module>util</module> <module>codegen</module> <module>common</module> <module>persistency</module> <module>web</module> </modules>
|
<modules> <module>util</module> <module>codegen</module> <module>common</module> <module>persistency</module> <module>web</module> </modules>
and he himself is listed as "pom"
In the codegen module there will be a generation code, and we will generate it in the common module.
Actually, the org.citymap.CommonTemplatesRunner class will generate
Therefore, in the common module we add the following:
<build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.5</version> <executions> <execution> <id>add-source</id> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>target/gencode</source> </sources> </configuration> </execution> </executions> </plugin> <plugin> <artifactId>maven-antrun-plugin</artifactId> <version>1.6</version> <executions> <execution> <phase> generate-sources </phase> <configuration> <target> <java classname="org.citymap.CommonTemplatesRunner"> <classpath refid="maven.dependency.classpath" /> <arg value="${project.build.directory}"/> </java> </target> </configuration> <goals> <goal>run</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
<build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.5</version> <executions> <execution> <id>add-source</id> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>target/gencode</source> </sources> </configuration> </execution> </executions> </plugin> <plugin> <artifactId>maven-antrun-plugin</artifactId> <version>1.6</version> <executions> <execution> <phase> generate-sources </phase> <configuration> <target> <java classname="org.citymap.CommonTemplatesRunner"> <classpath refid="maven.dependency.classpath" /> <arg value="${project.build.directory}"/> </java> </target> </configuration> <goals> <goal>run</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
<build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.5</version> <executions> <execution> <id>add-source</id> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>target/gencode</source> </sources> </configuration> </execution> </executions> </plugin> <plugin> <artifactId>maven-antrun-plugin</artifactId> <version>1.6</version> <executions> <execution> <phase> generate-sources </phase> <configuration> <target> <java classname="org.citymap.CommonTemplatesRunner"> <classpath refid="maven.dependency.classpath" /> <arg value="${project.build.directory}"/> </java> </target> </configuration> <goals> <goal>run</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
|
<build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.5</version> <executions> <execution> <id>add-source</id> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>target/gencode</source> </sources> </configuration> </execution> </executions> </plugin> <plugin> <artifactId>maven-antrun-plugin</artifactId> <version>1.6</version> <executions> <execution> <phase> generate-sources </phase> <configuration> <target> <java classname="org.citymap.CommonTemplatesRunner"> <classpath refid="maven.dependency.classpath" /> <arg value="${project.build.directory}"/> </java> </target> </configuration> <goals> <goal>run</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
As you can see, we have added a directory with the resources to the target. This is necessary so that when mvn clean, the generated code is erased (like all compilation products). And using anta, we run the code generator at the generate-sources stage. As an argument, we pass the directory to the generator where to generate the code (the current directory depends on whether the maven was launched in this project or in the dad)
Jamon
I use the
jamon framework for generation because it has a great maven plugin.
I add it to the codegen project like this:
<dependencies> <dependency> <groupId>org.jamon</groupId> <artifactId>jamon-runtime</artifactId> <version>2.3.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.5</version> <executions> <execution> <id>add-source</id> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>target/gencode</source> </sources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.jamon</groupId> <artifactId>jamon-maven-plugin</artifactId> <version>2.3.2</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>translate</goal> </goals> <configuration> <templateSourceDir>src/main/jamon</templateSourceDir> <templateOutputDir>target/gencode</templateOutputDir> </configuration> </execution> </executions> </plugin> </plugins> </build>
<dependencies> <dependency> <groupId>org.jamon</groupId> <artifactId>jamon-runtime</artifactId> <version>2.3.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.5</version> <executions> <execution> <id>add-source</id> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>target/gencode</source> </sources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.jamon</groupId> <artifactId>jamon-maven-plugin</artifactId> <version>2.3.2</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>translate</goal> </goals> <configuration> <templateSourceDir>src/main/jamon</templateSourceDir> <templateOutputDir>target/gencode</templateOutputDir> </configuration> </execution> </executions> </plugin> </plugins> </build>
<dependencies> <dependency> <groupId>org.jamon</groupId> <artifactId>jamon-runtime</artifactId> <version>2.3.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.5</version> <executions> <execution> <id>add-source</id> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>target/gencode</source> </sources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.jamon</groupId> <artifactId>jamon-maven-plugin</artifactId> <version>2.3.2</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>translate</goal> </goals> <configuration> <templateSourceDir>src/main/jamon</templateSourceDir> <templateOutputDir>target/gencode</templateOutputDir> </configuration> </execution> </executions> </plugin> </plugins> </build>
<dependencies> <dependency> <groupId>org.jamon</groupId> <artifactId>jamon-runtime</artifactId> <version>2.3.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.5</version> <executions> <execution> <id>add-source</id> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>target/gencode</source> </sources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.jamon</groupId> <artifactId>jamon-maven-plugin</artifactId> <version>2.3.2</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>translate</goal> </goals> <configuration> <templateSourceDir>src/main/jamon</templateSourceDir> <templateOutputDir>target/gencode</templateOutputDir> </configuration> </execution> </executions> </plugin> </plugins> </build>
|
<dependencies> <dependency> <groupId>org.jamon</groupId> <artifactId>jamon-runtime</artifactId> <version>2.3.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.5</version> <executions> <execution> <id>add-source</id> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>target/gencode</source> </sources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.jamon</groupId> <artifactId>jamon-maven-plugin</artifactId> <version>2.3.2</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>translate</goal> </goals> <configuration> <templateSourceDir>src/main/jamon</templateSourceDir> <templateOutputDir>target/gencode</templateOutputDir> </configuration> </execution> </executions> </plugin> </plugins> </build>
It generates code itself - based on templates in src / main / jamon, it will generate files that can be generated from templates in target / gencode. I hope no one is confused with the generation of generators :)
My template looks like this:
<% import > java.util.*; org.citymap.*; org.apache.commons.lang.StringUtils; </% import > <%args> Map<String, PointProperty> pojos; </%args> package org.citymap; import java.io.Serializable; public class PointAttributes implements Serializable { <% for String name:pojos.keySet () %> private boolean <% name%>; public boolean is<% StringUtils.capitalize ( name ) %> () { return <% name%>; } public boolean get<% StringUtils.capitalize ( name ) %> () { return <% name%>; } public void set<% StringUtils.capitalize ( name ) %> ( boolean value ) { <% name%> = value; } </% for > public boolean equals ( Object o ) { if ( this == o ) return true ; if ( o == null || getClass () != o.getClass ()) return false ; PointAttributes that = ( PointAttributes ) o; <% for String name:pojos.keySet () %> if ( <% name%> != that.<% name%> ) return false ; </% for > return true ; } public int hashCode () { int result = 0 ; <% for String name:pojos.keySet () %> result = 31 * result + ( <% name%> ? 1 : 0 ) ; </% for > return result; } } |
And I call it as follows:
public class CommonTemplatesRunner { public static void main ( String [] args ) throws IOException { String targetDir = args [ 0 ] ; final Reader csv = new InputStreamReader ( CodeGen. class .getClassLoader () .getResourceAsStream ( "properties.csv" )) ; final ObjectCSVMapper<PointProperty> csvProcessor = new ObjectCSVMapper<PointProperty> ( PointProperty.class, csv ) ; final Map<String, PointProperty> pojos = csvProcessor.getEntities () ; File dir = new File ( targetDir+ "/gencode/org/citymap" ) ; dir.mkdirs () ; FileWriter fw = new FileWriter ( new File ( dir, "PointAttributes.java" )) ; new PointAttributesTemplate () .render ( fw, pojos ) ; } } |
It remains to create an argument for it - this is the Map <String, PointProperty>.
I do this using the
supercsv library, generating PointProperty type objects from the properties.csv specified at the beginning.
So, we repeat the process:
Nothing starts up in the codegen module, only a code generator is created.
The PointAttributesTemplate class is created based on the PointAttributesTemplate.jamon template.
In the common module, the CommonTemplatesRunner is launched:
1. From properties.csv we make objects of type PointProperty.
2. Pass them as an argument to PointAttributesTemplate and it generates PointAttributes.java
Fuh. Everything.
Questions?