Showing posts with label maven2. Show all posts
Showing posts with label maven2. Show all posts

Friday, April 10, 2009

Maven2 plugin to generate shell scripts

Here is a silly, but potentially useful little Maven2 plugin that I wrote recently. It uses the application module's POM to build a bash script that runs a Java "executable" program (ie a class with a main() method).

For a given module, a bash script to run one class versus another is almost identical - the only difference is in the class name being passed to java. The rest of it is all boilerplate, and the largest portion is the CLASSPATH declaration. Since Java modules typically have tons of dependencies, it can be quite tedious to build the bash script by hand - in fact, it would probably qualify as my least favorite (coding) activity.

Why do it then, you ask? Well, I typically run small datasets (upto a couple 1000) through my program by writing a JUnit test and calling it using "mvn test", but I find that the JVM runs out of memory for large data sets, especially with a lot of logging. I suspect that this is because Maven2 buffers the logs in memory to write the results of the test in an XML file, but I could be wrong. The other reason is that if your program is going to production, then a shell script to run the code is one of the deliverables.

Plugin setup

The plugin is built inside an existing plugin project that I described in a previous post. For this plugin, I added in dependencies to JDOM and Velocity. The relevant snippet from my pom.xml is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<project>
  ...
  <dependencies>
  ...
    <dependency>
  ...
    <dependency>
      <groupId>velocity</groupId>
      <artifactId>velocity</artifactId>
      <version>1.4</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>jdom</groupId>
      <artifactId>jdom</artifactId>
      <version>1.0</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
  ...
</project>

Plugin code

Here is the code for the plugin. It is set to be called in the "deploy" phase, so it does not interfere with the normal development (clean compile test-compile test) life cycle.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// Source: src/main/java/com/mycompany/plugin/BashScriptMojo.java
package com.mycompany.plugin;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.Date;
import java.util.List;
import java.util.Properties;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.codehaus.plexus.util.StringUtils;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;

/**
 * Builds a script to execute a given class in a project.
 * @goal script
 * @phase deploy
 */
public class BashScriptMojo extends AbstractMojo {
  
  /**
   * Location of the file.
   * @parameter expression="${project.build.directory}"
   * @required
   * @readonly
   */
  private File outputDir;
  
  /**
   * The project directory where the pom.xml file is located.
   * @parameter expression="${basedir}"
   * @required
   * @readonly
   */
  private File projectDir;
  
  /**
   * Full class name to build execution script for.
   * @parameter expression="${className}"
   * @required
   */
  private String className;

  @SuppressWarnings("unchecked")
  public void execute() throws MojoExecutionException {
    try {
      // parse the pom.xml to find the list of dependency and expand
      // them out to the correct path in the M2 repository to build
      // the classpath
      SAXBuilder parser = new SAXBuilder();
      Document doc = parser.build(new File(projectDir, "pom.xml"));
      Element root = doc.getRootElement();
      Namespace defaultNamespace = root.getNamespace();
      Element dependenciesElement = 
        root.getChild("dependencies", defaultNamespace);
      StringBuilder buf = new StringBuilder();
      List<Element> dependencyElements =
        dependenciesElement.getChildren("dependency", defaultNamespace);
      for (Element dependencyElement : dependencyElements) {
        String groupId = dependencyElement.getChildTextTrim(
          "groupId", defaultNamespace);
        String artifactId = dependencyElement.getChildTextTrim(
          "artifactId", defaultNamespace);
        String version = dependencyElement.getChildTextTrim(
          "version", defaultNamespace);
        String path = StringUtils.join(new String[] {
          "$M2_REPO",
          StringUtils.replace(groupId, ".", File.separator),
          artifactId,
          version,
          StringUtils.join(new String[] {artifactId, version}, "-") + ".jar"
        }, File.separator);
        buf.append(path).append(File.pathSeparator).append("\\\n");
      }
      // finally append the target/classes dir
      buf.append(projectDir.getAbsolutePath()).append("/target/classes");
      // calculate the class name only for script file and log file
      String shortClassName = 
        className.substring(className.lastIndexOf('.') + 1);
      // stick them into the context
      VelocityContext context = new VelocityContext();
      context.put("__classpath__", buf.toString());
      context.put("__date__", new Date());
      context.put("__classname__", className);
      context.put("__logfile__", shortClassName + ".log");
      // we want to load the .vm file from the classpath, so we configure
      // the ClassPathResourceLoader to get the vm file.
      Properties props = new Properties();
      props.setProperty("resource.loader", "classpath");
      props.setProperty(
        "classpath.resource.loader.class", 
              "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
      Velocity.init(props);
      File scriptFile = new File(outputDir, "run" + shortClassName + ".sh");
      BufferedWriter writer = new BufferedWriter(new FileWriter(scriptFile));
      Velocity.mergeTemplate("bash_script.vm", "UTF-8", context, writer);
      writer.flush();
      writer.close();
      getLog().info("Script " + scriptFile.getName() + 
        " written to " + scriptFile.getPath());
    } catch (Exception e) {
      getLog().error("Error executing BashScriptMojo", e);
      e.printStackTrace();
      throw new MojoExecutionException(e.getMessage(), e);
    }
  }
}

Finally, here is the Velocity template file. As you can see above, I had to use Velocity's ClassPathResourceLoader to load it from the classpath (src/main/resources) of the plugin project.

1
2
3
4
5
#!/bin/bash
# Generated by mvn mycompany:script on ${__date__}
M2_REPO=$HOME/.m2/repository
CLASSPATH=${__classpath__}
java -cp $CLASSPATH -Xmx2048m ${__classname__} $* 2>&1 | tee ${__logfile__}

Plugin configuration

To install this into your local repository, run "mvn install:install". On the target module, where you actually want to use this plugin, you need to configure it in the module's POM as shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<project>
...
  <build>
  ...
    <plugins>
      ...
      <plugin>
        <groupId>com.mycompany.plugin</groupId>
        <artifactId>mycompany-maven-plugin</artifactId>
        <version>1.0-SNAPSHOT</version>
        <executions>
          <execution>
            <phase>deploy</phase>
            <goals>
              <goal>script</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

To call this plugin from the target application, run the following command:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
prompt$ mvn -o mycompany:script -DclassName=com.mycompany.foo.bar.Baz
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'mycompany'.
[INFO] -------------------------------------------------------------------
[INFO] Building MyCompany Plugin Module
[INFO]    task-segment: [mycompany:script]
[INFO] -------------------------------------------------------------------
[INFO] [mycompany:script]
[INFO] Script runBaz.sh written to /home/.../target/runBaz.sh
[INFO] -------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] -------------------------------------------------------------------
[INFO] Total time: 2 seconds
[INFO] Finished at: Fri Apr 10 16:24:33 GMT-08:00 2009
[INFO] Final Memory: 5M/9M
[INFO] -------------------------------------------------------------------

Which results in a shell script that looks something like this (edited for brevity):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/bash
# Generated by mvn mycompany:script on Fri Apr 10 16:24:33 GMT-08:00 2009
M2_REPO=$HOME/.m2/repository
CLASSPATH=\
$M2_REPO/commons-cli/commons-cli/1.0/commons-cli-1.0.jar:\
$M2_REPO/commons-codec/commons-codec/1.3/commons-codec-1.3.jar:\
$M2_REPO/commons-io/commons-io/1.2/commons-io-1.2.jar:\
...\
/home/.../target/classes
java -cp $CLASSPATH -Xmx2048m com.mycompany.foo.bar.Baz $* \
  2>&1 | tee Baz.log

Conclusion

Of course, the bash scripts that we send to production are not quite this simple - there is additional validation to make sure another process is not already running, and hooks to email completion status, etc, but they too are boilerplate. I have intentionally kept the script simple, but it is easy to update the template file to produce something that is more robust and production-ready.

The plugin does not handle transitive dependencies - which kind of defeats the purpose of using Maven2, I know... But since we have parallel Maven2 and Ant descriptors (some of our developers haven't gotten around to becoming comfortable with Maven2 yet), this is not an issue in my case, since we explicitly list all dependencies in both the Maven2 POM and Ant's build.xml files. However, I hope to update the plugin at some point to include Maven2's transitive dependency detection. If you have already done the research on how to do this, I would appreciate pointers.

On a completely unrelated note...

On a completely unrelated note, I found yesterday that Stacey's, my favorite bookstore (for around the last 7 years) has gone out of business. They were selling off the book cases when I arrived yesterday. Up until 3 years ago, my last job was about a couple of blocks from the store, so I was a pretty frequent visitor, and would average about one computer book a month. I have since moved to another location that is a good half hour walk (or a 10 minute bus ride), so I haven't been going as often. In spite of the higher prices compared to Amazon's discounted prices, I liked going to the bookshop and buying from there, since (a) I did not have to wait for the book to be shipped and (b) I could compare different books before making a purchase. Not sure about you, but Amazon's look-inside feature just doesn't compare.

There is a Borders across the street from where I work, but most of the time they don't have what I want, and even if they do, the books are so disorganized that its like finding a needle in a haystack. The Barnes and Noble where I live is more organized, but their focus (probably rightly so, given the demographics) is on children's books.

If anybody knows of a good bookshop that sells computer books in or around the San Francisco Market Street area, would appreciate hearing from you. Otherwise, I guess I will just have to get used to buying books online.

Friday, January 16, 2009

Extending Maven with Ant

Of late, I have been working with frameworks that seem to be doing an awful lot of bytecode manipulation, annotation processing and the like. Readers of past posts would already know about my experiments with Kilim and ActorFoundry, and I've recently started using JiBX for an application at work, which also does bytecode manipulation. If you've been reading my blog for a while, you'll also know that I'm a big Maven2 fan. I've been using Maven2 for almost couple of years (or more) now, and until recently, I hadn't really missed Ant that much.

I find that Maven2 makes standard build tasks trivial to non-existent, but non-standard tasks (for which there isn't already a plugin available) incredibly hard. With Ant, the level of effort is similar for a standard versus a non-standard task. This is because the design of Ant is imperative in nature, while Maven2's is declarative. With Ant, you tell it how to do a particular task using an XML based scripting language. With Maven2, you provide a standard project structure, and it knows how to do the standard tasks (called "goals" in Maven-speak). Not that I think this was a bad design decision, by the way - the declarative nature of Maven2 has served me (and I suspect most Maven2 users) quite well, with its automatic dependency management, standard goals, etc. And there are a huge number of plugins available - its only when you come across a situation where you need to roll your own is when you will have a problem.

Because I didn't know how to handle this sort of thing, my approach so far has been to build an Ant build.xml file from my Maven2 POM (using mvn ant:ant), and then add the non-standard task into the build.xml file. Of course, now anytime I need to add a new dependency into my pom, I have to add it in by hand into the build.xml. I still want to be able to generate IDE descriptors, build up the project on another machine, etc, so dispensing with the POM altogether is not an option.

One such example from the recent past (2 blog posts ago) is this little monster, refactored a bit for external access, detailing the steps to build my ActorFoundry and Kilim client code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
  <target name="compile" depends="get-deps" 
      description="Compile the code">
    <mkdir dir="${maven.build.output}"/>
    <javac srcdir="${maven.src.dir}"
           destdir="${maven.build.output}" 
           excludes="**/package.html" 
           debug="true" 
           deprecation="true" 
           optimize="false">
      <classpath refid="build.classpath"/>
    </javac>
    <ant target="check-local-constraints"/>
    <ant target="generate-af-executors"/>
    <ant target="compile-af-executors"/>
    <ant target="weave-classes"/>
  </target>
  <target name="check-local-constraints" 
      depends="_init" 
      description="Check local constraints">
    <!-- check local constraints happens for af only -->
    <apt 
         srcdir="${maven.src.dir}/${af.path.prefix}"
         compile="false"
         classpathref="build.classpath"
         debug="true"
         factory="osl.foundry.preprocessor.LocalSynchConstAPF"
         factorypathref="build.classpath"/>
  </target>
  <target name="generate-af-executors" 
      depends="_init" 
      description="Generate ActorFoundry Executors">
    <!-- code generation for af only -->
    <delete dir="${maven.src-gen.dir}"/>
    <mkdir dir="${maven.src-gen.dir}"/>
    <javadoc private="true"
         doclet="osl.foundry.preprocessor.ExecutorCodeGen"
         docletpathref="build.classpath"
         classpathref="build.classpath"
         sourcepath="${maven.src.dir}"
         packagenames="${af.pkg.prefix}">
      <arg line="-outdir ${maven.src-gen.dir}"/>
    </javadoc>
  </target>
  <target name="compile-af-executors" 
      depends="_init" 
      description="Compile ActorFoundry Executors">
    <!-- compile generated code: for af only -->
    <javac srcdir="${maven.src-gen.dir}"
           destdir="${maven.build.output}"
           debug="on"
           fork="on">
      <classpath refid="build.classpath"/>
    </javac>
  </target>
  <target name="weave-classes" 
      depends="_init" 
      description="Enhance classes using Kilim Weaver">
    <!-- weaving happens for kilim and af files -->
    <java classname="kilim.tools.Weaver" fork="yes">
      <classpath refid="weave.classpath"/>
      <assertions>
        <enable/>
      </assertions>
      <arg value="-x"/>
      <arg value="ExInvalid|test"/>
      <arg value="-d"/>
      <arg value="${maven.build.output}"/>
      <arg line="${kilim.pkg.prefix}.ActorManager 
                    ${kilim.pkg.prefix}.Actor 
                    ${kilim.pkg.prefix}.DownloadActor 
                    ${kilim.pkg.prefix}.IndexActor 
                    ${kilim.pkg.prefix}.WriteActor 
                    ${af.pkg.prefix}.ActorManagerExecutor
                    ${af.pkg.prefix}.DownloadActorExecutor
                    ${af.pkg.prefix}.IndexActorExecutor
                    ${af.pkg.prefix}.WriteActorExecutor"/>
    </java>
  </target>

As you can see, my compile target calls four other custom targets following the compilation phase. In Maven2's Default Build Lifecycle, these four targets would be called in the process-classes phase.

Approach #1: Build Custom Mojo(s)

The "pure" Maven way to address this is to build one or more MOJO (Maven pOJO) classes in Java that fires in the process-classes phase. This was my initial approach, which I later abandoned. However, in the process I learned some useful things, which I would like to describe here before going to my final solution.

Building a MOJO requires you to first build a Maven2 plugin project. The Plugin Developer's Guide page has quite a bit of information if you are interested. There is an archetype available for this, so you run:

1
2
3
4
5
prompt$ mvn archetype:create \
          -DgroupId=com.mycompany.plugins \
          -DartifactId=maven-mycompany-plugin \
          -DarchetypeGroupId=org.apache.maven.archetypes \
          -DarchetypeArtifactId=maven-archetype-mojo

This will create your project. Remove the url field from the POM, since this is going to be a local plugin. Since my MOJOs would need to walk directories and such, I needed commons-io (the recommended IO library for Maven) and plexus-utils (to access Plexus, the IoC container used by Maven), so I added them to the POM as shown below. I also made it Java 1.5 source/target compatible. Then I ran mvn eclipse:eclipse to generate the descriptors for Eclipse. The plugin project can then be opened in Eclipse as a standard Java project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  <dependencies>
    ...
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>1.4</version>
    </dependency>
    <dependency>
      <groupId>org.codehaus.plexus</groupId>
      <artifactId>plexus-utils</artifactId>
      <version>1.5.6</version>
    </dependency>
  </dependencies>

I started writing some code for a plugin that wrapped the Kilim Weaver. Essentially, it takes three parameters classpath, includes and excludes, and uses that to run the Weaver by calling java directly. I don't like this too much, but the alternative was to call the Exec plugin from the command line, which seemed much too heavyweight.

There are two popular books available on Maven, Better Builds with Maven and Maven: The Definitive Guide, (both free to download) and both have some information on how to build MOJOs, but you may have to peek at the sources of a similar plugin to figure out how to build your own. In my case, the sources for the Exec plugin were very helpful. Here is the code for the WeaverMojo.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// Project: maven-mycompany-plugin
// Source: src/main/java/com/mycompany/plugin/WeaverMojo.java
package com.mycompany.plugin;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.apache.commons.io.DirectoryWalker;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.util.cli.StreamConsumer;

/**
 * Maven Mojo for Kilim's Weaver.
 * 
 * @goal weave
 * @phase process-classes
 */
public class WeaverMojo extends AbstractMojo {
  
  /**
   * Input directory
   * @parameter default-value="${project.build.directory}"
   * @required
   * @readonly
   */
  private File inputDirectory;
  
  /**
   * Output directory
   * @parameter default-value="${project.build.directory}"
   * @required
   * @readonly
   */
  private File outputDirectory;
  
  /**
   * The Maven classpath, must be supplied manually in the config.
   * @parameter alias="classpath"
   * @required
   */
  private String mavenClassPath;
  
  /**
   * Specifies patterns to exclude. Multiple patterns can be specified
   * and are treated as exclude ${exclude[0]} OR ${exclude[1]} OR ...
   * @parameter alias="excludes"
   */
  private String[] excludes;

  /**
   * Specifies patterns to include. Multiple patterns can be specified
   * and are treated as include ${include[0]} OR ${include[1]} OR ...
   * @parameter alias="includes"
   */
  private String[] includes;
  
  public void execute() throws MojoExecutionException {
    getLog().info("Weaving classes...");
    WeaverDirectoryWalker walker = new WeaverDirectoryWalker();
    List<File> inputFiles = new ArrayList<File>();
    try {
      walker.walk(inputFiles);
    } catch (IOException e) {
      throw new MojoExecutionException("Problem walking directory", e);
    }
    // convert these from file name to package name notation
    List<String> classnames = new ArrayList<String>();
    String inputDirectoryPrefix = inputDirectory.getAbsolutePath(); 
    for (File inputFile : inputFiles) {
      classnames.add(inputFile.getAbsolutePath().
        replaceFirst(inputDirectoryPrefix, ""). // get rid of absolute path
        replaceFirst(".classes.", "").          // get rid of /classes/
        replaceAll("/", ".").                   // convert / to .
        replaceAll(".class", ""));              // remove trailing .class
    }
    // Call using java from command line
    Commandline commandline = new Commandline();
    commandline.setExecutable("java"); // assume that java is in PATH
    commandline.addArguments(new String[] {
      "-cp",
      mavenClassPath,
      "kilim.tools.weaver",
      "-x",
      "ExInvalid|Test",
      "-d",
      outputDirectory.getAbsolutePath(),
      StringUtils.join(classnames.iterator(), " ")
    });
    StreamConsumer stdout = new StreamConsumer() {
      public void consumeLine(String line) {
        getLog().info(line);
      }
    };
    StreamConsumer stderr = new StreamConsumer() {
      public void consumeLine(String line) {
        getLog().info( line );
      }
    };
    getLog().info("Running command: java " + 
      StringUtils.join(commandline.getArguments(), " "));  
    try {
      CommandLineUtils.executeCommandLine(commandline, stdout, stderr);
    } catch (CommandLineException e) {
      throw new MojoExecutionException("Java execution failed", e);
    }
  }
  
  private class WeaverDirectoryWalker extends DirectoryWalker {
    
    public WeaverDirectoryWalker() {
      super(new FileFilter() {
        public boolean accept(File f) {
          if (f.isDirectory()) {
            // we don't want directories in our list
            return true;
          }
          if (! f.getName().endsWith("class")) {
            // we only want .class files in our list
            return false;
          }
          if (f.getName().contains("$")) {
            // don't include inner class class files
            return false;
          }
          boolean included = false;
          boolean excluded = false;
          String filename = f.getAbsolutePath();
          if (includes != null) {
            for (int i = 0; i < includes.length; i++) {
              if (filename.matches(includes[i])) {
                getLog().info(filename + " == " + includes[i]);
                included = true;
                break;
              }
            }
          }
          if (excludes != null) {
            for (int i = 0; i < excludes.length; i++) {
              if (filename.matches(excludes[i])) {
                excluded = true;
                break;
              }
            }
          }
          return included && (! excluded);
        }
      }, -1);
    }

    public void walk(List<File> filenames) throws IOException {
      walk(inputDirectory, filenames);
    }
    
    @Override
    protected void handleFile(File file, int depth, Collection results) 
        throws IOException {
      results.add(file);
    }
  }
}

The work that the MOJO does is defined in its execute() method. The private member variables are annotated with commons-attribute annotations. Getting/setting the variables are handled by Plexus. The annotations are also used to generate the plugin.xml (plugin descriptor) file.

Incidentally, commons-io has a set of ready made FileFilters, which can be ANDed and ORed. It also has a RegexFilter, which uses Java regular expressions similar to my implementation. However, it applies the regular expression on the file name alone (not the entire path), so it did not work for me. It would be nicer to build up a composite filter using the AND/OR/NOT filters using my inputs and outputs arrays as the inputs and then pass it into the DirectoryWalker. It would perhaps also be nicer to be able to use an Ant style file filter in order to make the configuration easier to read, but I guess Java developers should be equally at home with either style.

To compile the MOJO, generate the plugin descriptor (plugin.xml) and install to your local repository, run mvn install.

On the client side (where you want to run the new plugin), you need to configure the build section with the plugin's configuration information. Here is the snippet from my client POM.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  <build>
    ...
    <plugins>
      ...
      <!-- WeaverMojo configuration -->
      <plugin>
        <groupId>com.mycompany.plugin</groupId>
        <artifactId>maven-mycompany-plugin</artifactId>
        <version>1.0-SNAPSHOT</version>
        <executions>
          <execution>
            <phase>process-classes</phase>
            <goals>
              <goal>weave</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <classpath>full runtime classpath here</classpath>
          <includes>
            <param>^.*kilim.*$</param>
            <param>^.*?actorfoundry.*?Executor.*$</param>
          </includes>
        </configuration>
      </plugin>
      ...
    </plugins>
    ...
  </build>

You can run this plugin using either mvn process-classes or mvn mycompany:weave (according to the docs, the second option is automatic if the plugin project's artifactId is either mycompany-maven-plugin or maven-mycompany-plugin). However, I had to add the project's groupId to my settings.xml file.

1
2
3
4
5
6
<settings>
  ...
  <pluginGroups>
    <pluginGroup>com.mycompany.plugin</pluginGroup>
  </pluginGroups>
</settings>

However, as mentioned before, I ultimately abandoned this approach in favor of the one described below. The plugin as described does fire in the appropriate place in the client's build lifecycle, but because the other components are missing, it does not help too much to run it.

Approach #2: Call Ant with AntRun

Looking through various Maven2 plugin sites for their source code, I came across the AntRun plugin. I had heard of it in the past, but did not like the idea of having to depend on Ant. However, given my newly found knowledge of "pure" Maven2 plugins, AntRun seemed to be a ready-made solution to my problem.

The first step was refactoring the extra steps in the compile target into separate Ant targets so they could be called individually, as shown in the build.xml snippet above. The second step was simply add the AntRun plugin descriptor and its configuration into the client POM. There were two gotchas here, however:

  1. Ant's properties were not getting initialized from build.xml, in spite of setting the inheritRefs attribute to true.
  2. Apt was not getting recognized as a valid Ant task, even though its a core task in Ant 1.7.1.

The first problem is easily solved. When mvn ant:ant is used to generate the build.xml, the properties are stored as globals (i.e. within the scope of the project tag), which don't get initialized when called with <ant target="..."/>. The workaround was to create a separate _init target which wrapped the property initialization, and make all tasks dependent on _init at the lowest level (ie, if a task has no dependencies, then it should depend on _init now. You will notice that all our tasks have the depends="_init" set in the build.xml snippet above.

The second problem took me a while to figure out. Apparently, AntRun uses an internal version of Ant (the default version is 1.6.5), and Apt was not a core Ant task in that version. To reset the version, you have to inject the correct version of Ant jars in the plugin descriptor - many thanks to Jason Lee for this post, which I reached through this JIRA page.

The plugin descriptor for AntRun to run the tasks in the process-classes phase is quite simple and self-explanatory, and is shown below. To run this, you need to do mvn process-classes (no fancy aliases here).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
  <build>
    ...
    <plugins>
      ...
      <!-- Antrun plugin -->
      <plugin>
        <artifactId>maven-antrun-plugin</artifactId>
        <executions>
          <execution>
            <phase>process-classes</phase>
            <configuration>
              <tasks>
                <property name="user.home" value="${user.home}"/>
                <ant antfile="build.xml" 
                  target="check-local-constraints" 
                  inheritRefs="true"/>
                <ant antfile="build.xml" 
                  target="generate-af-executors" 
                  inheritRefs="true"/>
                <ant antfile="build.xml" 
                  target="compile-af-executors" 
                  inheritRefs="true"/>
                <ant antfile="build.xml" 
                  target="weave-classes" 
                  inheritRefs="true"/>
              </tasks>
            </configuration>
            <goals>
              <goal>run</goal>
            </goals>
          </execution>
        </executions>
        <dependencies>
          <dependency>
            <groupId>org.apache.ant</groupId>
            <artifactId>ant</artifactId>
            <version>1.7.1</version>
          </dependency>
          <dependency>
            <groupId>org.apache.ant</groupId>
            <artifactId>ant-launcher</artifactId>
            <version>1.7.1</version>
          </dependency>
          <dependency>
            <groupId>org.apache.ant</groupId>
            <artifactId>ant-nodeps</artifactId>
            <version>1.7.1</version>
          </dependency>
          <dependency>
            <groupId>org.apache.ant</groupId>
            <artifactId>ant-apache-bsf</artifactId>
            <version>1.7.1</version>
          </dependency>
          <dependency>
            <groupId>org.apache.bsf</groupId>
            <artifactId>bsf-all</artifactId>
            <version>3.0-beta2</version>
          </dependency>
          <dependency>
            <groupId>rhino</groupId>
            <artifactId>js</artifactId>
            <version>1.7R1</version>
          </dependency>
        </dependencies>
      </plugin>
      ...
    </plugins>
    ...
  </build>

One document that you may find useful if you go the AntRun route is this list of properties accessible in the POM, which you can pass to Ant's task.

There is a lot of XML in here (more than if I went the MOJO route), but no extra plugin code to write. I also don't have to maintain both Ant and Maven2 XMLs simultaneously, which was my original gripe. The ant tasks here call the target in build.xml, but I could just as easily have built a standalone XML file which contained the scripts to do the various tasks, and which would not do the standard stuff such as compile, jar, etc.

Conclusions

When building goals involving third party components, where you either don't have visibility into or control of the source code, it may be preferable to use the AntRun plugin. Ant, notwithstanding its limitations (some of which Maven2 addresses), is likely to be with us for the forseeable future, so it makes sense to leverage it if it makes sense.

However, for goals involving internal components, building MOJO based custom Maven2 plugins would probably provide more flexibility and remove the need for having two build frameworks in place in an organization. That is really the reason I went as far into developing the WeaverMojo as I did, to provide me with an understanding of how to build a Maven2 custom plugin should I need to at some point in the future.

Its paradoxical that Maven offers a built-in feature to facilitate project documentation (mvn site), yet it is harder to find information for Maven than for Ant, which does not offer any such feature. In all fairness, all the Maven plugin projects I have looked, are,without exception, very well documented. However, there is no one-stop shop such as the Ant manual.

Saturday, July 07, 2007

WebApp Scaffolds with Java, Spring and Maven2

Most dynamic web applications depend on a lot of data stored in databases, and ours is no exception. As the site grows in complexity, our current approach of maintaining the data using SQL is becoming less and less tenable. There are more tables than there were before, and one must understand the relationships before being able to update data. There is a crying need for simple CRUD based application data maintenance tools that operate on a set of one or more tables. However, because of the need to deliver our applications on tight deadlines, we frequently have no time to build these tools, so we end up like the shoemaker's children who have no shoes.

While this may seem like we are saving time by omitting non-essential items in our deliverables schedule, it's actually a really dumb move for several reasons. One, each time you have to add or otherwise maintain your application data, you have to go back and try to understand the data structure and rewrite the SQL to do it. This takes time, so essentially you are paying in time and effort over the life of the project what you would have paid up front...many times over. Two, because the process is manual, you will make mistakes at some point, and some of the mistakes may be catastrophic. Three, without a tool, you are stuck making the updates, since nobody but an engineer is insane enough to consider an SQL editor a long term data maintenance tool. With a web-based tool, if you are lucky, you can pass the work off to the person who needs the data changes in the first place. Even if you are not lucky and you are stuck making the changes yourself, life will still be easier with a tool.

I started working on a little personal project to build a project reporting system couple of weeks ago. It's fully database driven, the application will provide little more than an interface to the database. I had started working on a little framework, inspired by Ruby on Rails (RoR) about 2 years ago, that would generate most of the basic application, but I had shelved it because I did not have much use for it and I was not making much progress. I did consider existing Java based Rails-like frameworks in Java (Sails, Grails), but I decided to build my own because both Sails and Grails are rather large and hard to understand, and use all sorts of cool software which I don't need or care about. They also generate the application into a different structure than I expect. The framework is described in some detail below:

Because there is quite a lot of code (at least more than is possible to show in this blog entry), and there is a chance that I may enhance it in the future, I have applied for a SourceForge project to host the code. If I get it, the project would be called AutoCRUD, and the source code would be available there. Check there about a week from the date of this post.

The code is generated using the information in the database. For that reason, the table and column names must follow certain conventions, so this may be impractical to use for an existing project as is. However, the code is reasonably simple, so it may be possible to make it use a different set of naming conventions that you may follow consistently on your database. The database naming conventions the code depends on are listed below:

  1. Entities are represented by singular table names, eg. project, person, etc. Table names for tables representing entities must not contain underscores.
  2. Join tables that join two entities must be named with the two entity names separated by underscore, eg. project_person. These tables will have a single underscore.
  3. All tables must have an id column. The column data type must resolve to a Java long type. Different databases use different type mappings, for MySQL it is bigint(20). These columns should be defined as auto-incrementing (for MySQL, no other database is supported at the moment). Databases that support sequences (such as Oracle or PostgreSQL) should have an appropriately named sequence, but thats only for future compatibility issues.
  4. Foreign key reference columns must be named ${entityName}_id where entityName is the table which is being referenced from this table using the foreign key. Again, this does not matter at the moment, but may come up in the future.

So based on the rules above, I start off with the following schema for my database:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
create table project (
  id bigint(20) unsigned not null auto_increment,
  name varchar(32) not null,
  description varchar(256) not null,
  start_dt timestamp not null,
  end_dt timestamp,
  primary key(id)
);

create table person (
  id bigint(20) unsigned not null auto_increment,
  name varchar(32) not null,
  primary key(id)
);

create table project_person (
  id bigint(20) unsigned not null auto_increment,
  project_id bigint(20),
  person_id bigint(20),
  primary key(id)
);
create unique index ux1_project_person on project_person(project_id, person_id);
alter table project_person 
  add constraint fk_project foreign key(project_id) references project(id);
alter table project_person
  add constraint fk_person foreign key(person_id) references person(id);

create table task (
  id bigint(20) unsigned not null auto_increment,
  project_id bigint(20) not null,
  seq_id varchar(8) not null,
  name varchar(64) not null,
  est_hrs integer not null,
  primary key(id)
);
create unique index ux1_task on task(project_id, seq_id);
alter table task
  add constraint fk_project foreign key(project_id) references project(id);

create table hour (
  id bigint(20) unsigned not null auto_increment,
  task_id bigint(20) not null,
  log_date date not null,
  act_hrs integer not null,
  primary key(id)
);
create unique index ux1_hour on hour(task_id, log_date);
alter table hour 
  add constraint fk_task foreign key(task_id) references task(id);

The following artifacts need to be generated to build the Maven2 Spring web application.

  1. The web.xml file. This will be in src/main/webapp/WEB-INF and will contain the servlet definition for the Spring DispatcherServlet.
  2. The Spring application context file (*-servlet.xml). This would live in src/main/webapp/WEB-INF and contain all the ActiveRecord and controller definitions.
  3. The index.jsp landing page.
  4. The ActiveRecord subclasses for each individual tables in the schema. These will be packaged under the project package in the beans package.
  5. The list.jsp, show.jsp and edit.jsp files for each individual table. These will be placed in the src/main/webapp/ directory, each set of three JSP files will be in its own subdirectory that is the table name, eg. src/main/webapp/project/list.jsp.

All the templates for the various files are provided as Velocity .vm files, so they can be changed if you don't like my coding style, indentation, etc. I just show the generated code for a single entity project to keep this blog post to a reasonable size.

The bean subclass for the project entity looks like this. As you can see, internally its just a Map structure. The generation process gives it formal getters and setters which can be accessed from JSTL in the JSPs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Project extends ActiveRecord {

  public Project() {
    super();
    setFields(new String[] {
      "name",
      "description",
      "start_dt",
      "end_dt",
      "id"
    });
    setTableName("project");
  }

  public void setName(String inputValue) {
    setValue("name", inputValue);
  }
  
  public String getName() {
    return (String) getValue("name");
  }
  public void setDescription(String inputValue) {
    setValue("description", inputValue);
  }
  
  public String getDescription() {
    return (String) getValue("description");
  }
  public void setStartDt(java.sql.Timestamp inputValue) {
    setValue("start_dt", inputValue);
  }
  
  public java.sql.Timestamp getStartDt() {
    return (java.sql.Timestamp) getValue("start_dt");
  }
  public void setEndDt(java.sql.Timestamp inputValue) {
    setValue("end_dt", inputValue);
  }
  
  public java.sql.Timestamp getEndDt() {
    return (java.sql.Timestamp) getValue("end_dt");
  }
}

The code for ActiveRecord is shown below. It provides some methods that will be called from the generated code, and database persistence methods such as save(), delete(), findAll(), findById() and findBy() methods.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
public class ActiveRecord extends JdbcDaoSupport {

  private Map<String,Object> fields = new HashMap<String,Object>();
  private String tableName;

  /**
   * Called from within the generated setter methods in subclasses.
   * @param fieldName the database field name.
   * @param value the value.
   */
  public void setValue(String fieldName, Object value) {
    fields.put(fieldName, value);
  }

  /**
   * Called from within the generated getter methods in subclasses.
   * @param fieldName the database field name.
   * @return the value.
   */
  public Object getValue(String fieldName) {
    return fields.get(fieldName);
  }

  /**
   * Set the database column names. This is set by the generated subclasses
   * in their constructor.
   * @param fieldNames the database column names.
   */
  public void setFields(String[] fieldNames) {
    for (String fieldName : fieldNames) {
      if (fields.containsKey(fieldName)) {
        continue;
      }
      fields.put(fieldName, null);
    }
  }
  
  public Set<String> getFields() {
    return fields.keySet();
  }
  
  /**
   * Set the database table name. This is set by the generated subclasses
   * in their constructor.
   * @param tableName the database table name.
   */
  public void setTableName(String tableName) {
    this.tableName = tableName;
  }
  
  public String getTableName() {
    return tableName;
  }
  
  public List<ActiveRecord> find(String condition) throws Exception {
    if (StringUtils.isBlank(condition)) {
      return findAll();
    }
    Set<String> colNames = fields.keySet();
    StringBuilder queryBuilder = new StringBuilder();
    queryBuilder.append("select ").
      append(StringUtils.join(colNames.iterator(), ',')).
      append(" from ").
      append(getTableName()).
      append(" where ").append(condition);
    return getActiveRecords(queryBuilder.toString());
  }

  public List<ActiveRecord> findAll() throws Exception {
    Set<String> colNames = fields.keySet();
    StringBuilder queryBuilder = new StringBuilder();
    queryBuilder.append("select ").
      append(StringUtils.join(colNames.iterator(), ',')).
      append(" from ").
      append(getTableName());
    return getActiveRecords(queryBuilder.toString());
  }

  public ActiveRecord findById(long id) throws Exception {
    List<ActiveRecord> records = find("id=" + id);
    if (records.size() == 0) {
      return null;
    }
    if (records.size() > 1) {
      throw new Exception(getTableName() + ".id must be a primary key");
    }
    return records.get(0);
  }

  public long getId() {
    Object oid = fields.get("id");
    if (oid == null) {
      return 0L;
    }
    return new Long(oid.toString());
  }

  public long delete() throws Exception {
    StringBuilder queryBuilder = new StringBuilder();
    queryBuilder.append("delete from ").
      append(getTableName()).
      append(" where id=").
      append(getId());
    getJdbcTemplate().update(queryBuilder.toString());
    return getId();
  }

  public long save() throws Exception {
    ActiveRecord dbRecord = findById(getId());
    StringBuilder queryBuilder = new StringBuilder();
    if (this.equals(dbRecord)) {
      // record exists, do update
      queryBuilder.append("update ").
        append(getTableName()).
        append(" set ");
      Set<String> colNames = fields.keySet();
      Object[] params = new Object[colNames.size() - 1];
      int i = 0;
      for (String colName : colNames) {
        if (colName.equals("id")) {
          continue;
        }
        if (i > 0) {
          queryBuilder.append(",");
        }
        queryBuilder.append(colName).append("=?");
        params[i] = fields.get(colName);
        i++;
      }
      queryBuilder.append(" where id=").append(getId());
      getJdbcTemplate().update(queryBuilder.toString(), params);
    } else {
      // record is new, do insert
      Set<String> colNames = fields.keySet();
      Object[] params = new Object[colNames.size()];
      queryBuilder.append("insert into ").append(getTableName()).append("(");
      StringBuilder columnListBuilder = new StringBuilder();
      StringBuilder placeHolderBuilder = new StringBuilder();
      int i = 0;
      for (String colName : colNames) {
        if (i > 0) {
          columnListBuilder.append(",");
          placeHolderBuilder.append(",");
        }
        columnListBuilder.append(colName);
        placeHolderBuilder.append("?");
        params[i] = fields.get(colName);
        i++;
      }
      queryBuilder.append(columnListBuilder.toString()).
        append(")values(").
        append(placeHolderBuilder.toString()).
        append(")");
      getJdbcTemplate().update(queryBuilder.toString(), params);
      long id = getJdbcTemplate().queryForLong("select max(id) from " + getTableName());
      setValue("id", id);
    }
    return getId();
  }

  @Override
  public int hashCode() {
    return (int) getId();
  }
  
  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof ActiveRecord)) {
      return false;
    }
    ActiveRecord that = (ActiveRecord) obj;
    return (this.getId() == that.getId());
  }

  protected ActiveRecord newInstance(String className) throws Exception {
    Object obj = Class.forName(className).newInstance();
    if (obj instanceof ActiveRecord) {
      ActiveRecord activeRecord = (ActiveRecord) obj;
      activeRecord.setDataSource(getDataSource());
      activeRecord.setValue("id", new Long(0L));
      return activeRecord;
    } else {
      throw new Exception("Class:" + className + " must extend ActiveRecord");
    }
  }

  @SuppressWarnings("unchecked")
  private List<ActiveRecord> getActiveRecords(String query) throws Exception {
    List<Map<String,Object>> rows = getJdbcTemplate().queryForList(query);
    List<ActiveRecord> records = new ArrayList<ActiveRecord>();
    Set<String> colNames = fields.keySet();
    for (Map<String,Object> row : rows) {
      ActiveRecord record = newInstance(this.getClass().getName());
      for (String colName : colNames) {
        record.setValue(colName, row.get(colName));
      }
      records.add(record);
    }
    return records;
  }
}

We wrap the ActiveRecord instance in a transactional proxy, so its save() and delete() operations are transactional. The full bean definition is shown below. The definitions are automatically generated by the scaffold generation code. References to external beans such as dataSource and transactionManager are set up (by the generator) prior to declaring the bean.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  <bean id="project" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <property name="target">
      <bean class="net.sf.prozac.app.beans.Project" scope="prototype">
        <property name="dataSource" ref="dataSource"/>
      </bean>
    </property>
    <property name="transactionManager" ref="transactionManager"/>
    <property name="proxyTargetClass" value="true"/>
    <property name="transactionAttributes">
      <props>
        <prop key="*">PROPAGATION_REQUIRED,-Exception</prop>
        <prop key="find*">PROPAGATION_SUPPORTS</prop>
      </props>
    </property>
  </bean>

Sample usage of the Project ActiveRecord is shown from my JUnit test below. The usage is slightly less natural than if you had used a separate DAO for this bean, but it is not really hard to follow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    Project project = (Project) context.getBean("project");
    project.setName("Rocket Launcher");
    project.setDescription("A test project to do rocket launching");
    project.setStartDt(new Timestamp(System.currentTimeMillis()));
    project.setEndDt(new Timestamp(System.currentTimeMillis()));
    long id = project.save();
    logger.debug("project.id=" + id);
    List<ActiveRecord> projects = project.findAll();
    for (ActiveRecord record : projects) {
      Project p1 = (Project) record;
      logger.debug("project.id=" + p1.getId());
      logger.debug("project.name=" + p1.getName());
      logger.debug("project.description=" + p1.getDescription());
      logger.debug("project.startDt=" + p1.getStartDt());
      logger.debug("project.endDt=" + p1.getEndDt());
      p1.delete();
    }

The ActiveController is a standard Spring MultiActionController which enforces the navigation logic. It uses a ParameterMethodName resolver, and the initial and default action is the list view. The action parameter determines which method is invoked. The code for the ActiveController is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public class ActiveController extends MultiActionController {

  private ActiveRecord activeRecord;
  private String tableName;
  
  public void setActiveRecord(ActiveRecord activeRecord) {
    this.activeRecord = activeRecord;
  }
  
  public void setTableName(String tableName) {
    this.tableName = tableName;
  }
  
  public ModelAndView list(HttpServletRequest request, HttpServletResponse response) throws Exception {
    List<ActiveRecord> records = activeRecord.findAll();
    ModelAndView mav = new ModelAndView();
    mav.addObject("records", records);
    mav.addObject("fields", activeRecord.getFields());
    mav.setViewName(StringUtils.join(new String[] {tableName, "list"}, '/'));
    return mav;
  }
  
  public ModelAndView add(HttpServletRequest request, HttpServletResponse response) throws Exception {
    Set<String> fieldNames = activeRecord.getFields();
    ActiveRecord record = activeRecord.newInstance(activeRecord.getClass().getName());
    ModelAndView mav = new ModelAndView();
    mav.addObject("record", record);
    mav.setViewName(StringUtils.join(new String[] {tableName, "edit"}, '/'));
    return mav;
  }
  
  public ModelAndView edit(HttpServletRequest request, HttpServletResponse response) throws Exception {
    long id = ServletRequestUtils.getRequiredLongParameter(request, "id");
    ActiveRecord record = activeRecord.findById(id);
    ModelAndView mav = new ModelAndView();
    mav.addObject("record", record);
    mav.setViewName(StringUtils.join(new String[] {tableName, "edit"}, '/'));
    return mav;
  }
  
  public ModelAndView save(HttpServletRequest request, HttpServletResponse response) throws Exception {
    Set<String> fieldNames = activeRecord.getFields();
    ActiveRecord record = activeRecord.newInstance(activeRecord.getClass().getName());
    for (String fieldName : fieldNames) {
      String value = ServletRequestUtils.getStringParameter(request, fieldName);
      record.setValue(fieldName, value);
    }
    record.save();
    return list(request, response);
  }
  
  public ModelAndView remove(HttpServletRequest request, HttpServletResponse response) throws Exception {
    long id = ServletRequestUtils.getRequiredLongParameter(request, "id");
    ActiveRecord record = activeRecord.findById(id);
    record.delete();
    return list(request, response);
  }
  
  public ModelAndView show(HttpServletRequest request, HttpServletResponse response) throws Exception {
    long id = ServletRequestUtils.getRequiredLongParameter(request, "id");
    ActiveRecord record = activeRecord.findById(id);
    ModelAndView mav = new ModelAndView();
    mav.addObject("record", record);
    mav.addObject("fields", activeRecord.getFields());
    mav.setViewName(StringUtils.join(new String[] {tableName, "show"}, '/'));
    return mav;
  }
  
  public ModelAndView search(HttpServletRequest request, HttpServletResponse response) throws Exception {
    StringBuilder conditionBuilder = new StringBuilder();
    Set<String> fieldNames = activeRecord.getFields();
    for (String fieldName : fieldNames) {
      String value = ServletRequestUtils.getStringParameter(request, fieldName);
      if (StringUtils.isBlank(value)) {
        continue;
      }
      conditionBuilder.append(fieldName + "='" + value + "'");
    }
    List<ActiveRecord> records = activeRecord.find(conditionBuilder.toString());
    ModelAndView mav = new ModelAndView();
    mav.addObject("records", records);
    mav.addObject("fields", activeRecord.getFields());
    mav.setViewName(StringUtils.join(new String[] {tableName, "list"}, '/'));
    return mav;
  }
}

Currently, each ActiveRecord controller instance uses the ActiveController directly, although in keeping with RoR style, these should be empty subclasses which the user can override if desired. Here is the bean configuration for the projectController:

1
2
3
4
5
6
7
8
9
  <bean id="projectController" class="net.sf.prozac.framework.ActiveController">
    <property name="activeRecord" ref="project"/>
    <property name="tableName" value="project"/>
    <property name="methodNameResolver">
      <bean class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
        <property name="defaultMethodName" value="list"/>
      </bean>
    </property>
  </bean>

The three JSPs that are generated per table (or entity bean) are the list.jsp, edit.jsp and the show.jsp files. They live in their own subdirectory, named after the table, in the src/main/webapp directory. They are shown below:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<!-- project/list.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
    
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>project:list</title>
</head>
<body>
  <h1>project:list</h1>
  <table cellspacing="0" cellpadding="0" border="1" width="100%">
    <tr>
      <th>name</th>
      <th>description</th>
      <th>start_dt</th>
      <th>end_dt</th>
      <th>View</th>
    </tr>
    <c:forEach items="${records}" var="record">
    <tr>
      <td>${record.name}</td>
      <td>${record.description}</td>
      <td>${record.startDt}</td>
      <td>${record.endDt}</td>
      <td><a href="/prozac/project.do?action=show&id=${record.id}">View</a></td>
    </tr>
    </c:forEach>
  </table>
  <br/>
  <a href="/prozac/project.do?action=add">Add</a>
</body>
</html>

<!-- project/edit.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>project:edit</title>
</head>
<body>
  <h1>project:edit</h1>
  <form name="edit" action="/prozac/project.do" method="post">
    <input type="hidden" name="action" value="save"/>
    <input type="hidden" name="id" value="${record.id}"/>
    <table cellspacing="0" cellpadding="0" border="0" width="100%">
      <tr>
        <td><b>name</b></td>
        <td><input type="text" name="name" value="${record.name}"/></td>
      </tr>
      <tr>
        <td><b>description</b></td>
        <td><input type="text" name="description" value="${record.description}"/></td>
      </tr>
      <tr>
        <td><b>start_dt</b></td>
        <td><input type="text" name="startDt" value="${record.startDt}"/></td>
      </tr>
      <tr>
        <td><b>end_dt</b></td>
        <td><input type="text" name="endDt" value="${record.endDt}"/></td>
      </tr>
    </table>
    <input type="submit" name="submit" value="Submit"/>&nbsp;&nbsp;
    <input type="button" name="cancel" value="Cancel" onclick="javascript:window.location='/prozac/project.do?action=list'"/>
  </form>
</body>
</html>

<!-- project/show.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>project:show</title>
</head>
<body>
  <h1>project:show</h1>
  <table cellspacing="0" cellpadding="0" border="0" width="100%">
    <tr>
      <td><b>name</b></td>
      <td>${record.name}</td>
    </tr>
    <tr>
      <td><b>description</b></td>
      <td>${record.description}</td>
    </tr>
    <tr>
      <td><b>start_dt</b></td>
      <td>${record.startDt}</td>
    </tr>
    <tr>
      <td><b>end_dt</b></td>
      <td>${record.endDt}</td>
    </tr>
  </table>
  <input type="button" name="edit" value="Edit" onclick="javascript:window.location='/prozac/project.do?action=edit&id=${record.id}'"/>&nbsp;&nbsp;
  <input type="button" name="delete" value="Delete" onclick="javascript:window.location='/prozac/project.do?action=remove&id=${record.id}'"/>&nbsp;&nbsp;
  <input type="button" name="cancel" value="Cancel" onclick="javascript:window.location='/prozac/project.do?action=list'"/>
</body>
</html>

The generated files can be run right away without any editing, but admittedly the pages are not very pretty. At this point, it is fairly easy to just manually prettify it up. Alternatively, if we standardize on how the pages should look, and what widgets should be used for particular column data types, we could modify the velocity template files to produce this. So after generating all the files, I fire up Jetty using Maven's jetty6:run goal and point to localhost:8080/prozac (my application name).

Application Index Page
Project Lists Page (no entries currently). Click Add link to add a project.
Add Project Page. Cancel will send you back to list page, Submit will add the record and send you back to the list page (below).
Project List page with a single entry. Click on the View link to see the single project record.
Project view page. Click on Edit to edit the contents, delete to delete the contents and cancel to do nothing and return to the list. We select edit.
Project Edit page. We change the description and click the Submit button.
The Project List page with the change applied.

There are quite a few things that can be improved with this framework. First off, it works only against the MySQL database, which is the database I have on my laptop at the moment. I will need to make it work with Oracle if I want to use this at work. The other major improvement is in the use of appropriate widgets for various data types. I am not an expert on that end, so I guess I will have to get someone who is good at that to take a look there. Yet another thing I want to work on is to be able to specify associations, which both RoR and the Java RoR-like frameworks allow but AutoCRUD does not.

Update (July 15, 2007): I have uploaded the code described in the blog to the AutoCRUD project on Sourceforge.