Invariant Properties

  • rss
  • Home

Creating Sonarqube Projects

Bear Giles | January 27, 2014

Sonarqube (nee Sonar) is da bomb. It’s not something you have to check daily but if you’re serious about quality you’ll check it during sprint planning if not weekly.

Check out a sample project at nemo.sonarqube.com, e.g., OpenJPA, to get an idea of what information is available. You might want to focus on a specific component at first, e.g., OpenJPA JDBC.

As a developer I’m mostly interested in the “issues” (mostly FindBugs and Squid) and “unit test coverage.” As an architect I’m mostly interested in the “package tangle index” and “complexity” – the former is a measure of proper encapsulation and decoupling and the latter is a measure of maintainability.

It’s important to view these numbers with an appropriate amount of salt. They give valuable insights but it takes a bit of experience to make the best use of them. That’s why it’s important to keep this information away from the bean counters who will set unreasonable standards like 90% code coverage in all unit tests. (This can be impossible to achieve if you have rich exception handling but no way to mock the classes that will throw those exceptions. Only a fool would trade code robustness for a higher score.)

Installing Sonarqube

It’s straightforward to install sonarqube. It comes bundled with its own webapp server and embedded database so you can check it out by just unpacking it and running the startup script. A production system should use a real database. Multiple databases are supported.

Check the sonarqube site for details.

Creating Our Project

I’ll admit it – creating a project is very counter-intuitive. In a nutshell everything is handled by pushing data to the server without creating anything on the sonarqube server first. (You’ll still want to create admin users on the sonarqube server.)

In practice this means that we add a maven plugin. This is an expensive plugin so it’s common to use a custom profile, e.g., “sonar” (for the legacy name).

  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2.     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3.  
  4.     <profiles>
  5.         <profile>
  6.             <id>sonar</id>
  7.             <properties>
  8.                 <sonar.language>java</sonar.language>
  9.                 <sonar.host.url>http://chaos:9000</sonar.host.url>
  10.                 <sonar.jdbc.url>jdbc:postgresql://chaos/sonar</sonar.jdbc.url>
  11.                 <sonar.jdbc.username>sonar</sonar.jdbc.username>
  12.                 <sonar.jdbc.password>sonar</sonar.jdbc.password>
  13.             </properties>
  14.             <build>
  15.                 <plugins>
  16.                     <plugin>
  17.                         <groupId>org.jacoco</groupId>
  18.                         <artifactId>jacoco-maven-plugin</artifactId>
  19.                         <version>0.6.4.201312101107</version>
  20.                         <executions>
  21.                             <execution>
  22.                                 <id>default-prepare-agent</id>
  23.                                 <goals>
  24.                                     <goal>prepare-agent</goal>
  25.                                 </goals>
  26.                             </execution>
  27.                             <execution>
  28.                                 <id>default-prepare-agent-integration</id>
  29.                                 <goals>
  30.                                     <goal>prepare-agent-integration</goal>
  31.                                 </goals>
  32.                             </execution>
  33.                             <execution>
  34.                                 <id>default-report</id>
  35.                                 <goals>
  36.                                     <goal>report</goal>
  37.                                 </goals>
  38.                             </execution>
  39.                             <execution>
  40.                                 <id>default-report-integration</id>
  41.                                 <goals>
  42.                                     <goal>report-integration</goal>
  43.                                 </goals>
  44.                             </execution>
  45.                             <execution>
  46.                                 <id>default-check</id>
  47.                                 <goals>
  48.                                     <goal>check</goal>
  49.                                 </goals>
  50.                                 <configuration>
  51.                                     <rules>
  52.                                         <!-- implmentation is needed only for Maven 2 -->
  53.                                         <rule implementation="org.jacoco.maven.RuleConfiguration">
  54.                                             <element>BUNDLE</element>
  55.                                             <limits>
  56.                                                 <!-- implmentation is needed only for Maven 2 -->
  57.                                                 <limit implementation="org.jacoco.report.check.Limit">
  58.                                                     <counter>COMPLEXITY</counter>
  59.                                                     <value>COVEREDRATIO</value>
  60.                                                     <minimum>0.60</minimum>
  61.                                                 </limit>
  62.                                             </limits>
  63.                                         </rule>
  64.                                     </rules>
  65.                                 </configuration>
  66.                             </execution>
  67.                         </executions>
  68.                     </plugin>
  69.                 </plugins>
  70.             </build>
  71.         </profile>
  72.     </profiles>
  73. </project>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <profiles>
        <profile>
            <id>sonar</id>
            <properties>
                <sonar.language>java</sonar.language>
                <sonar.host.url>http://chaos:9000</sonar.host.url>
                <sonar.jdbc.url>jdbc:postgresql://chaos/sonar</sonar.jdbc.url>
                <sonar.jdbc.username>sonar</sonar.jdbc.username>
                <sonar.jdbc.password>sonar</sonar.jdbc.password>
            </properties>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.jacoco</groupId>
                        <artifactId>jacoco-maven-plugin</artifactId>
                        <version>0.6.4.201312101107</version>
                        <executions>
                            <execution>
                                <id>default-prepare-agent</id>
                                <goals>
                                    <goal>prepare-agent</goal>
                                </goals>
                            </execution>
                            <execution>
                                <id>default-prepare-agent-integration</id>
                                <goals>
                                    <goal>prepare-agent-integration</goal>
                                </goals>
                            </execution>
                            <execution>
                                <id>default-report</id>
                                <goals>
                                    <goal>report</goal>
                                </goals>
                            </execution>
                            <execution>
                                <id>default-report-integration</id>
                                <goals>
                                    <goal>report-integration</goal>
                                </goals>
                            </execution>
                            <execution>
                                <id>default-check</id>
                                <goals>
                                    <goal>check</goal>
                                </goals>
                                <configuration>
                                    <rules>
                                        <!-- implmentation is needed only for Maven 2 -->
                                        <rule implementation="org.jacoco.maven.RuleConfiguration">
                                            <element>BUNDLE</element>
                                            <limits>
                                                <!-- implmentation is needed only for Maven 2 -->
                                                <limit implementation="org.jacoco.report.check.Limit">
                                                    <counter>COMPLEXITY</counter>
                                                    <value>COVEREDRATIO</value>
                                                    <minimum>0.60</minimum>
                                                </limit>
                                            </limits>
                                        </rule>
                                    </rules>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

Updating Our Project

The sonar plugin is expensive so it should not be run as part of a routine build. A common practice is to schedule a nightly build on the CI server (Hudson, Continuum, etc.) Developers may also want to perform off-schedule builds while working on the issue backlog – it’s not uncommon for one fix to introduce other lower-priority issues.

Source Code

A sample project using this plugin is at https://github.com/beargiles/project-student [github] and http://beargiles.github.io/project-student/ [github pages].

This project illustrates the need to be intelligent about how we interpret the results. I use two common practices – throwing an internal exception instead of returning a null value and using a custom ‘UnitTestException’ to test failure code without cluttering the logs with extraneous information. The code looks the same as questionable code so it’s properly flagged but there doesn’t seem to be a way to silence squid warnings. (Findbugs has its own SuppressWarnings annotation.)

Overall it’s still a huge win.

(Update: it’s possible to control the squid warnings via the sonarqube ‘Quality Profiles’ tab. This can be used to reduce the severity level to ‘info’ but I’m hesitant to disable these tests outright since they are sometimes legitimate warnings. That’s why I strongly prefer to use per-instance FindBugs SuppressWarnings annotations instead of changing those warning levels.)

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

Creating Maven Source and Javadoc Artifacts

Bear Giles | January 27, 2014

Many people are aware of maven source and javadoc artifacts but don’t know why they would want to create them. I was definitely in this camp – I can see why people want this information but it seemed like a relatively inefficient way to get it since it requires manual navigation of the maven repository.

Then I got hit by a clue stick.

These artifacts are used by IDEs, not people. If you’re using maven dependencies the IDE is smart enough to know how to find these artifacts. The source artifact is used when you’re single-stepping through code in the debugger – you no longer have to explicitly bind source code to libraries in the IDE. The javadoc artifact is used for auto-completion and context-sensitive help in the editor.

Neither of these is required – I was happy using ‘vi’ for many years – but it definitely improves your productivity when you mostly know what you need but aren’t quite sure of the details.

Source Artifacts

The source artifacts are the easiest to create. Add a plugin that will be run automatically as part of a standard build and you’re done. Builds take slightly longer but it’s probably not enough to worry about since you’re just creating an archive of a few directories.

  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2.    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3.     <build>
  4.         <plugins>
  5.             <!-- create source jar -->
  6.             <plugin>
  7.                 <groupId>org.apache.maven.plugins</groupId>
  8.                 <artifactId>maven-source-plugin</artifactId>
  9.                 <version>2.1.1</version>
  10.                 <executions>
  11.                     <execution>
  12.                         <id>attach-sources</id>
  13.                         <phase>verify</phase>
  14.                         <goals>
  15.                             <!-- produce source artifact for project main sources -->
  16.                             <goal>jar-no-fork</goal>
  17.                             <!-- produce test source artifact for project test sources -->
  18.                             <goal>test-jar-no-fork</goal>
  19.                         </goals>
  20.                     </execution>
  21.                 </executions>
  22.             </plugin>
  23.         </plugins>
  24.     </build>
  25. </project>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <build>
        <plugins>
            <!-- create source jar -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>2.1.1</version>
                <executions>
                    <execution>
                        <id>attach-sources</id>
                        <phase>verify</phase>
                        <goals>
                            <!-- produce source artifact for project main sources -->
                            <goal>jar-no-fork</goal>
                            <!-- produce test source artifact for project test sources -->
                            <goal>test-jar-no-fork</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Javadoc Artifacts

Javadoc artifacts are a little more complex since you’ll probably want to create a human-friendly site at the same time. The biggest issue there, in my experience, is that external classes were opaque since it took so much effort to create the necessary links. The maven plugin now takes care of that for us!

This artifact takes a significant amount of time to build so you probably don’t want to do it every time. There are two approaches – either specify the maven target explicitly or tie it to a custom profile, e.g., ‘javadoc’. The configuration below uses a custom profile.

  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2.    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3.  
  4.         <profiles>
  5.             <!-- create javadoc -->
  6.             <profile>
  7.                 <id>javadoc</id>
  8.                 <build>
  9.                     <plugins>
  10.                         <plugin>
  11.                             <groupId>org.apache.maven.plugins</groupId>
  12.                         <artifactId>maven-javadoc-plugin</artifactId>
  13.                         <version>2.9.1</version>
  14.                         <configuration>
  15.                             <detectLinks />
  16.                             <includeDependencySources>true</includeDependencySources>
  17.                             <dependencySourceIncludes>
  18.                                 <dependencySourceInclude>com.invariantproperties.project.student:*</dependencySourceInclude>
  19.                             </dependencySourceIncludes>
  20.                             <!-- heavily used dependencies -->
  21.                             <links>
  22.                                 <link>http://docs.oracle.com/javase/7/docs/api/</link>
  23.                                 <link>http://docs.oracle.com/javaee/6/api</link>
  24.                                 <link>http://docs.spring.io/spring/docs/current/javadoc-api/</link>
  25.                                 <link>http://docs.spring.io/spring-data/commons/docs/1.6.2.RELEASE/api/</link>
  26.                                 <link>http://docs.spring.io/spring-data/jpa/docs/1.4.3.RELEASE/api/</link>
  27.                                 <link>http://docs.spring.io/spring-data/data-jpa/docs/1.4.3.RELEASE/api/</link>
  28.                                 <link>https://jersey.java.net/apidocs/1.17/jersey/</link>
  29.                                 <link>http://hamcrest.org/JavaHamcrest/javadoc/1.3/</link>
  30.                                 <link>http://eclipse.org/aspectj/doc/released/runtime-api/</link>
  31.                                 <link>http://eclipse.org/aspectj/doc/released/weaver-api</link>
  32.                                 <link>http://tapestry.apache.org/5.3.7/apidocs/</link>
  33.                             </links>
  34.                         </configuration>
  35.                         <executions>
  36.                             <execution>
  37.                                 <id>aggregate</id>
  38.                                 <!-- <phase>site</phase> -->
  39.                                 <phase>package</phase>
  40.                                 <goals>
  41.                                     <goal>aggregate</goal>
  42.                                     <goal>jar</goal>
  43.                                 </goals>
  44.                             </execution>
  45.                         </executions>
  46.                     </plugin>
  47.                 </plugins>
  48.             </build>
  49.  
  50.             <reporting>
  51.                 <plugins>
  52.                     <plugin>
  53.                         <groupId>org.apache.maven.plugins</groupId>
  54.                         <artifactId>maven-javadoc-plugin</artifactId>
  55.                         <version>2.9.1</version>
  56.                         <configuration>
  57.                             <!-- Default configuration for all reports -->
  58.                         </configuration>
  59.                         <reportSets>
  60.                             <reportSet>
  61.                                 <id>non-aggregate</id>
  62.                                 <configuration>
  63.                                     <!-- Specific configuration for the non aggregate report -->
  64.                                 </configuration>
  65.                                 <reports>
  66.                                     <report>javadoc</report>
  67.                                 </reports>
  68.                             </reportSet>
  69.                             <reportSet>
  70.                                 <id>aggregate</id>
  71.                                 <configuration>
  72.                                     <!-- Specific configuration for the aggregate report -->
  73.                                 </configuration>
  74.                                 <reports>
  75.                                     <report>aggregate</report>
  76.                                 </reports>
  77.                             </reportSet>
  78.                         </reportSets>
  79.                     </plugin>
  80.                 </plugins>
  81.             </reporting>
  82.         </profile>
  83.     </profiles>
  84. </project>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

        <profiles>
            <!-- create javadoc -->
            <profile>
                <id>javadoc</id>
                <build>
                    <plugins>
                        <plugin>
                            <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-javadoc-plugin</artifactId>
                        <version>2.9.1</version>
                        <configuration>
                            <detectLinks />
                            <includeDependencySources>true</includeDependencySources>
                            <dependencySourceIncludes>
                                <dependencySourceInclude>com.invariantproperties.project.student:*</dependencySourceInclude>
                            </dependencySourceIncludes>
                            <!-- heavily used dependencies -->
                            <links>
                                <link>http://docs.oracle.com/javase/7/docs/api/</link>
                                <link>http://docs.oracle.com/javaee/6/api</link>
                                <link>http://docs.spring.io/spring/docs/current/javadoc-api/</link>
                                <link>http://docs.spring.io/spring-data/commons/docs/1.6.2.RELEASE/api/</link>
                                <link>http://docs.spring.io/spring-data/jpa/docs/1.4.3.RELEASE/api/</link>
                                <link>http://docs.spring.io/spring-data/data-jpa/docs/1.4.3.RELEASE/api/</link>
                                <link>https://jersey.java.net/apidocs/1.17/jersey/</link>
                                <link>http://hamcrest.org/JavaHamcrest/javadoc/1.3/</link>
                                <link>http://eclipse.org/aspectj/doc/released/runtime-api/</link>
                                <link>http://eclipse.org/aspectj/doc/released/weaver-api</link>
                                <link>http://tapestry.apache.org/5.3.7/apidocs/</link>
                            </links>
                        </configuration>
                        <executions>
                            <execution>
                                <id>aggregate</id>
                                <!-- <phase>site</phase> -->
                                <phase>package</phase>
                                <goals>
                                    <goal>aggregate</goal>
                                    <goal>jar</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>

            <reporting>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-javadoc-plugin</artifactId>
                        <version>2.9.1</version>
                        <configuration>
                            <!-- Default configuration for all reports -->
                        </configuration>
                        <reportSets>
                            <reportSet>
                                <id>non-aggregate</id>
                                <configuration>
                                    <!-- Specific configuration for the non aggregate report -->
                                </configuration>
                                <reports>
                                    <report>javadoc</report>
                                </reports>
                            </reportSet>
                            <reportSet>
                                <id>aggregate</id>
                                <configuration>
                                    <!-- Specific configuration for the aggregate report -->
                                </configuration>
                                <reports>
                                    <report>aggregate</report>
                                </reports>
                            </reportSet>
                        </reportSets>
                    </plugin>
                </plugins>
            </reporting>
        </profile>
    </profiles>
</project>

package-info.java

Finally each package should have a package-info.java file. This replaces the old package-info.html file but is an improvement since it allows the use of class annotations. (It will not be compiled since it’s an invalid class name.)

I’ve found it extremely helpful to include links to resources that help me understand why the classes look the way they do. For instance the metadata package in my ‘student’ project contains links to my blog posts, other blog posts that I’ve found useful, and even the appropriate Oracle tutorial. At a company these could be links to wiki pages.

  1. /**                    
  2.  * Classes that support JPA Criteria Queries for persistent entities.
  3.  *                          
  4.  * @see <a href="http://invariantproperties.com/2013/12/19/project-student-persistence-with-spring-data/">Project Student: Persistence with Spring Data</a>
  5.  * @see <a href="http://invariantproperties.com/2013/12/29/project-student-jpa-criteria-queries/">Project Student: JPA Criteria Queries</a>
  6.  * @see <a href="http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-four-jpa-criteria-queries/">Spring Data JPA Tutorial Part Four: JPA Criteria Queries</a> [www.petrikainulainen.net]
  7.  * @see <a href="http://docs.oracle.com/javaee/6/tutorial/doc/gjitv.html">JEE Tutorial</a>      
  8.  *  
  9.  * @author Bear Giles <bgiles@coyotesong.com>
  10.  */            
  11. package com.invariantproperties.project.student.metamodel;
/**                     
 * Classes that support JPA Criteria Queries for persistent entities.
 *                          
 * @see <a href="http://invariantproperties.com/2013/12/19/project-student-persistence-with-spring-data/">Project Student: Persistence with Spring Data</a>
 * @see <a href="http://invariantproperties.com/2013/12/29/project-student-jpa-criteria-queries/">Project Student: JPA Criteria Queries</a>
 * @see <a href="http://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-four-jpa-criteria-queries/">Spring Data JPA Tutorial Part Four: JPA Criteria Queries</a> [www.petrikainulainen.net]
 * @see <a href="http://docs.oracle.com/javaee/6/tutorial/doc/gjitv.html">JEE Tutorial</a>      
 *  
 * @author Bear Giles <bgiles@coyotesong.com>
 */             
package com.invariantproperties.project.student.metamodel;

Source Code

The source code is at https://github.com/beargiles/project-student [github] and http://beargiles.github.io/project-student/ [github pages].

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

Project Student Moved To Github

Bear Giles | January 27, 2014

A quick note – Project Student has moved to Git Hub. I’ll be updating the blog posts here (but not at Java Code Geeks) to point to it soon. The package names have also been refactored so it’s in ‘project’ instead of ‘sandbox’.

Github

Github Project Pages.

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

Project Student: Simplifying Code With AOP

Bear Giles | January 9, 2014

This is part of Project Student.

Many people strongly believe that methods should fit within your editor window (say, 20 lines), and some people believe that methods should be even smaller than that. The idea is that a method should do one thing and only one thing. If it does more than that it should be broken apart into multiple methods and the old method’s “one thing” is to coordinate the new methods.

This does not mean breaking apart a method after an arbitrary number of lines. Sometimes methods are naturally larger. It’s still always a good question to ask.

So how do you recognize code that does more than one thing? A good touchstone is if the code is duplicated in multiple methods. The canonical example is transaction management in persistence classes. Every persistence class needs it and the code always looks the same.

Another example is the unhandled exception handler in my Resource classes. Every REST-facing method needs to deal with this and the code always looks the same.

That’s the theory. In practice the code can be ugly and with modest gains. Fortunately there’s a solution: Aspect-Oriented Programming (AOP). This allows us to transparently weave code before or after our method calls. This often lets us simplify our methods dramatically.

Design Decisions

AspectJ – I’m using AspectJ via Spring injection.

Limitations

The AspectJ pointcut expressions are relatively simple with the CRUD methods. That might not be true as more complex functionality is added.

Unhandled Exceptions in Resource Methods

Our first concern is unhandled exceptions in resource methods. Jersey will return a SERVER INTERNAL ERROR (500) message anyway but it will probably include a stack trace and other content that we don’t want an attacker to know. We can control what it includes if we send it ourselves. We could add a ‘catch’ block in all of our methods but it’s boilerplate that we can move into an AOP method. That will leave all of our Resource methods a lot slimmer and easier to read.

This class also checks for “Object Not Found” exceptions. It would be easy to handle in the individual Resource classes but it muddies the code. Putting the exception handler here allows our methods to focus on the happy path and guarantees consistency in the response.

This class has two optimizations. First, it explicitly checks for a UnitTestException and skips detailed logging in that case. One of my biggest pet peeve is tests that that flood the logs with stack traces when everything is working like expected. That makes it impossible to skim the logs for obvious problems. This single change can make problems much easier to find.

Second, it uses the logger associated with the targeted class, e.g., CourseResource, instead of with the AOP class. Besides being clearer this allows us to selectively change the logging level for a single Resource instead of all of them.

Another trick is to call an ExceptionService in our handler. This is a service that can do something useful with exceptions, e.g., it might create or update a Jira ticket. This hasn’t been implemented so I just put in a comment to show where it goes.

  1. @Aspect
  2. @Component
  3. public class UnexpectedResourceExceptionHandler {
  4.     @Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource)")
  5.     public Object checkForUnhandledException(ProceedingJoinPoint pjp) throws Throwable {
  6.         Object results = null;
  7.         Logger log = Logger.getLogger(pjp.getSignature().getClass());
  8.  
  9.         try {
  10.             results = pjp.proceed(pjp.getArgs());
  11.         } catch (ObjectNotFoundException e) {
  12.             // this is safe to log since we know that we've passed filtering.
  13.             String args = Arrays.toString(pjp.getArgs());
  14.             results = Response.status(Status.NOT_FOUND).entity("object not found: " + args).build();
  15.             if (log.isDebugEnabled()) {
  16.                 log.debug("object not found: " + args);
  17.             }
  18.         } catch (Exception e) {
  19.             // find the method we called. We can't cache this since the method
  20.             // may be overloaded
  21.             Method method = findMethod(pjp);
  22.             if ((method != null) && Response.class.isAssignableFrom(method.getReturnType())) {
  23.                 // if the method returns a response we can return a 500 message.
  24.                 if (!(e instanceof UnitTestException)) {
  25.                     if (log.isInfoEnabled()) {
  26.                         log.info(
  27.                                 String.format("%s(): unhandled exception: %s", pjp.getSignature().getName(),
  28.                                         e.getMessage()), e);
  29.                     }
  30.                 } else if (log.isTraceEnabled()) {
  31.                     log.info("unit test exception: " + e.getMessage());
  32.                 }
  33.                 results = Response.status(Status.INTERNAL_SERVER_ERROR).build();
  34.             } else {
  35.                 // DO NOT LOG THE EXCEPTION. That just clutters the log - let
  36.                 // the final handler log it.
  37.                 throw e;
  38.             }
  39.         }
  40.  
  41.         return results;
  42.     }
  43.  
  44.     /**
  45.      * Find method called via reflection.
  46.      */
  47.     Method findMethod(ProceedingJoinPoint pjp) {
  48.         Class[] argtypes = new Class[pjp.getArgs().length];
  49.         for (int i = 0; i < argtypes.length; i++) {
  50.             argtypes[i] = pjp.getArgs()[i].getClass();
  51.         }
  52.  
  53.         Method method = null;
  54.  
  55.         try {
  56.             // @SuppressWarnings("unchecked")
  57.             method = pjp.getSignature().getDeclaringType().getMethod(pjp.getSignature().getName(), argtypes);
  58.         } catch (Exception e) {
  59.             Logger.getLogger(UnexpectedResourceExceptionHandler.class).info(
  60.                     String.format("could not find method for %s.%s", pjp.getSignature().getDeclaringType().getName(),
  61.                             pjp.getSignature().getName()));
  62.         }
  63.  
  64.         return method;
  65.     }
  66. }
@Aspect
@Component
public class UnexpectedResourceExceptionHandler {
    @Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource)")
    public Object checkForUnhandledException(ProceedingJoinPoint pjp) throws Throwable {
        Object results = null;
        Logger log = Logger.getLogger(pjp.getSignature().getClass());

        try {
            results = pjp.proceed(pjp.getArgs());
        } catch (ObjectNotFoundException e) {
            // this is safe to log since we know that we've passed filtering.
            String args = Arrays.toString(pjp.getArgs());
            results = Response.status(Status.NOT_FOUND).entity("object not found: " + args).build();
            if (log.isDebugEnabled()) {
                log.debug("object not found: " + args);
            }
        } catch (Exception e) {
            // find the method we called. We can't cache this since the method
            // may be overloaded
            Method method = findMethod(pjp); 
            if ((method != null) && Response.class.isAssignableFrom(method.getReturnType())) {
                // if the method returns a response we can return a 500 message.
                if (!(e instanceof UnitTestException)) {
                    if (log.isInfoEnabled()) {
                        log.info(
                                String.format("%s(): unhandled exception: %s", pjp.getSignature().getName(),
                                        e.getMessage()), e);
                    }
                } else if (log.isTraceEnabled()) {
                    log.info("unit test exception: " + e.getMessage());
                }
                results = Response.status(Status.INTERNAL_SERVER_ERROR).build();
            } else {
                // DO NOT LOG THE EXCEPTION. That just clutters the log - let
                // the final handler log it.
                throw e;
            }
        }

        return results;
    }

    /**
     * Find method called via reflection.
     */
    Method findMethod(ProceedingJoinPoint pjp) {
        Class[] argtypes = new Class[pjp.getArgs().length];
        for (int i = 0; i < argtypes.length; i++) {
            argtypes[i] = pjp.getArgs()[i].getClass();
        }

        Method method = null;

        try {
            // @SuppressWarnings("unchecked")
            method = pjp.getSignature().getDeclaringType().getMethod(pjp.getSignature().getName(), argtypes);
        } catch (Exception e) {
            Logger.getLogger(UnexpectedResourceExceptionHandler.class).info(
                    String.format("could not find method for %s.%s", pjp.getSignature().getDeclaringType().getName(),
                            pjp.getSignature().getName()));
        }

        return method;
    }
}

REST Post Values Checking

Our Resource methods also have a lot of boilerplate code to check the REST parameters. Are they non-null, are email addresses well-formed, etc. Again it’s easy to move much of this code into an AOP method and simplify the Resource method.

We start by defining an interface that indicates a REST transfer object can be validated. This first version gives us a simple thumbs-up or thumbs-down, an improved version will give us a way to tell the client what the specific problems are.

  1. public interface Validatable {
  2.  
  3.     boolean validate();
  4. }
public interface Validatable {

    boolean validate();
}

We now extend our earlier REST transfer objects to add a validation method.

Two notes. First, the name and email address accept Unicode letters, not just the standard ASCII letters. This is important as our world becomes internationalized.

Second, I’ve added a toString() method but it’s unsafe since it uses unsanitized values. I’ll address sanitization later.

  1. @XmlRootElement
  2. public class NameAndEmailAddressRTO implements Validatable {
  3.  
  4.     // names must be alphabetic, an apostrophe, a dash or a space. (Anne-Marie,
  5.     // O'Brien). This pattern should accept non-Latin characters.
  6.     // digits and colon are added to aid testing. Unlikely but possible in real
  7.     // names.
  8.     private static final Pattern NAME_PATTERN = Pattern.compile("^[\\p{L}\\p{Digit}' :-]+$");
  9.  
  10.     // email address must be well-formed. This pattern should accept non-Latin
  11.     // characters.
  12.     private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@]+@([\\p{L}\\p{Digit}-]+\\.)?[\\p{L}]+");
  13.  
  14.     private String name;
  15.     private String emailAddress;
  16.     private String testUuid;
  17.  
  18.     public String getName() {
  19.         return name;
  20.     }
  21.  
  22.     public void setName(String name) {
  23.         this.name = name;
  24.     }
  25.  
  26.     public String getEmailAddress() {
  27.         return emailAddress;
  28.     }
  29.  
  30.     public void setEmailAddress(String emailAddress) {
  31.         this.emailAddress = emailAddress;
  32.     }
  33.  
  34.     public String getTestUuid() {
  35.         return testUuid;
  36.     }
  37.  
  38.     public void setTestUuid(String testUuid) {
  39.         this.testUuid = testUuid;
  40.     }
  41.  
  42.     /**
  43.      * Validate values.
  44.      */
  45.     @Override
  46.     public boolean validate() {
  47.         if ((name == null) || !NAME_PATTERN.matcher(name).matches()) {
  48.             return false;
  49.         }
  50.  
  51.         if ((emailAddress == null) || !EMAIL_PATTERN.matcher(emailAddress).matches()) {
  52.             return false;
  53.         }
  54.  
  55.         if ((testUuid != null) && !StudentUtil.isPossibleUuid(testUuid)) {
  56.             return false;
  57.         }
  58.  
  59.         return true;
  60.     }
  61.  
  62.     @Override
  63.     public String toString() {
  64.         // FIXME: this is unsafe!
  65.         return String.format("NameAndEmailAddress('%s', '%s', %s)", name, emailAddress, testUuid);
  66.     }
  67. }
@XmlRootElement
public class NameAndEmailAddressRTO implements Validatable {

    // names must be alphabetic, an apostrophe, a dash or a space. (Anne-Marie,
    // O'Brien). This pattern should accept non-Latin characters.
    // digits and colon are added to aid testing. Unlikely but possible in real
    // names.
    private static final Pattern NAME_PATTERN = Pattern.compile("^[\\p{L}\\p{Digit}' :-]+$");

    // email address must be well-formed. This pattern should accept non-Latin
    // characters.
    private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@]+@([\\p{L}\\p{Digit}-]+\\.)?[\\p{L}]+");

    private String name;
    private String emailAddress;
    private String testUuid;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmailAddress() {
        return emailAddress;
    }

    public void setEmailAddress(String emailAddress) {
        this.emailAddress = emailAddress;
    }

    public String getTestUuid() {
        return testUuid;
    }

    public void setTestUuid(String testUuid) {
        this.testUuid = testUuid;
    }

    /**
     * Validate values.
     */
    @Override
    public boolean validate() {
        if ((name == null) || !NAME_PATTERN.matcher(name).matches()) {
            return false;
        }

        if ((emailAddress == null) || !EMAIL_PATTERN.matcher(emailAddress).matches()) {
            return false;
        }

        if ((testUuid != null) && !StudentUtil.isPossibleUuid(testUuid)) {
            return false;
        }

        return true;
    }

    @Override
    public String toString() {
        // FIXME: this is unsafe!
        return String.format("NameAndEmailAddress('%s', '%s', %s)", name, emailAddress, testUuid);
    }
}

We make similar changes to the other REST transfer objects.

We can now write our AOP methods to check the parameters to our CRUD operations. As before the logs are written using the logger associated with the Resource instead of the AOP class.

These methods also log the entry of the Resource methods. Again it’s boilerplate and doing it here simplifies the Resource methods. It would be trivial to also log the method’s exit and elapsed time but we should use a stock logger AOP class in that case.

  1. @Aspect
  2. @Component
  3. public class CheckPostValues {
  4.  
  5.     /**
  6.      * Check post values on create method.
  7.      *
  8.      * @param pjp
  9.      * @return
  10.      * @throws Throwable
  11.      */
  12.     @Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource) && args(validatable,..)")
  13.     public Object checkParametersCreate(ProceedingJoinPoint pjp, Validatable rto) throws Throwable {
  14.         final Logger log = Logger.getLogger(pjp.getSignature().getDeclaringType());
  15.         final String name = pjp.getSignature().getName();
  16.         Object results = null;
  17.  
  18.         if (rto.validate()) {
  19.             // this should be safe since parameters have been validated.
  20.             if (log.isDebugEnabled()) {
  21.                 log.debug(String.format("%s(%s): entry", name, Arrays.toString(pjp.getArgs())));
  22.             }
  23.             results = pjp.proceed(pjp.getArgs());
  24.         } else {
  25.             // FIXME: this is unsafe
  26.             if (log.isInfoEnabled()) {
  27.                 log.info(String.format("%s(%s): bad arguments", name, Arrays.toString(pjp.getArgs())));
  28.             }
  29.             // TODO: tell caller what the problems were
  30.             results = Response.status(Status.BAD_REQUEST).build();
  31.         }
  32.  
  33.         return results;
  34.     }
  35.  
  36.     /**
  37.      * Check post values on update method.
  38.      *
  39.      * @param pjp
  40.      * @return
  41.      * @throws Throwable
  42.      */
  43.     @Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource) && args(uuid,validatable,..)")
  44.     public Object checkParametersUpdate(ProceedingJoinPoint pjp, String uuid, Validatable rto) throws Throwable {
  45.         final Logger log = Logger.getLogger(pjp.getSignature().getDeclaringType());
  46.         final String name = pjp.getSignature().getName();
  47.         Object results = null;
  48.  
  49.         if (!StudentUtil.isPossibleUuid(uuid)) {
  50.             // this is a possible attack.
  51.             if (log.isInfoEnabled()) {
  52.                 log.info(String.format("%s(): uuid", name));
  53.             }
  54.             results = Response.status(Status.BAD_REQUEST).build();
  55.         } else if (rto.validate()) {
  56.             // this should be safe since parameters have been validated.
  57.             if (log.isDebugEnabled()) {
  58.                 log.debug(String.format("%s(%s): entry", name, Arrays.toString(pjp.getArgs())));
  59.             }
  60.             results = pjp.proceed(pjp.getArgs());
  61.         } else {
  62.             // FIXME: this is unsafe
  63.             if (log.isInfoEnabled()) {
  64.                 log.info(String.format("%s(%s): bad arguments", name, Arrays.toString(pjp.getArgs())));
  65.             }
  66.             // TODO: tell caller what the problems were
  67.             results = Response.status(Status.BAD_REQUEST).build();
  68.         }
  69.  
  70.         return results;
  71.     }
  72.  
  73.     /**
  74.      * Check post values on delete method. This is actually a no-op but it
  75.      * allows us to log method entry.
  76.      *
  77.      * @param pjp
  78.      * @return
  79.      * @throws Throwable
  80.      */
  81.     @Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource) && args(uuid,version) && execution(* *.delete*(..))")
  82.     public Object checkParametersDelete(ProceedingJoinPoint pjp, String uuid, Integer version) throws Throwable {
  83.         final Logger log = Logger.getLogger(pjp.getSignature().getDeclaringType());
  84.         final String name = pjp.getSignature().getName();
  85.         Object results = null;
  86.  
  87.         if (!StudentUtil.isPossibleUuid(uuid)) {
  88.             // this is a possible attack.
  89.             if (log.isInfoEnabled()) {
  90.                 log.info(String.format("%s(): uuid", name));
  91.             }
  92.             results = Response.status(Status.BAD_REQUEST).build();
  93.         } else {
  94.             // this should be safe since parameters have been validated.
  95.             if (log.isDebugEnabled()) {
  96.                 log.debug(String.format("%s(%s): entry", name, Arrays.toString(pjp.getArgs())));
  97.             }
  98.             results = pjp.proceed(pjp.getArgs());
  99.         }
  100.  
  101.         return results;
  102.     }
  103.  
  104.     /**
  105.      * Check post values on find methods. This is actually a no-op but it allows
  106.      * us to log method entry.
  107.      *
  108.      * @param pjp
  109.      * @return
  110.      * @throws Throwable
  111.      */
  112.     @Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource) && execution(* *.find*(..))")
  113.     public Object checkParametersFind(ProceedingJoinPoint pjp) throws Throwable {
  114.         final Logger log = Logger.getLogger(pjp.getSignature().getDeclaringType());
  115.  
  116.         if (log.isDebugEnabled()) {
  117.             log.debug(String.format("%s(%s): entry", pjp.getSignature().getName(), Arrays.toString(pjp.getArgs())));
  118.         }
  119.         final Object results = pjp.proceed(pjp.getArgs());
  120.  
  121.         return results;
  122.     }
  123. }
@Aspect
@Component
public class CheckPostValues {

    /**
     * Check post values on create method.
     * 
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource) && args(validatable,..)")
    public Object checkParametersCreate(ProceedingJoinPoint pjp, Validatable rto) throws Throwable {
        final Logger log = Logger.getLogger(pjp.getSignature().getDeclaringType());
        final String name = pjp.getSignature().getName();
        Object results = null;

        if (rto.validate()) {
            // this should be safe since parameters have been validated.
            if (log.isDebugEnabled()) {
                log.debug(String.format("%s(%s): entry", name, Arrays.toString(pjp.getArgs())));
            }
            results = pjp.proceed(pjp.getArgs());
        } else {
            // FIXME: this is unsafe
            if (log.isInfoEnabled()) {
                log.info(String.format("%s(%s): bad arguments", name, Arrays.toString(pjp.getArgs())));
            }
            // TODO: tell caller what the problems were
            results = Response.status(Status.BAD_REQUEST).build();
        }

        return results;
    }

    /**
     * Check post values on update method.
     * 
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource) && args(uuid,validatable,..)")
    public Object checkParametersUpdate(ProceedingJoinPoint pjp, String uuid, Validatable rto) throws Throwable {
        final Logger log = Logger.getLogger(pjp.getSignature().getDeclaringType());
        final String name = pjp.getSignature().getName();
        Object results = null;

        if (!StudentUtil.isPossibleUuid(uuid)) {
            // this is a possible attack.
            if (log.isInfoEnabled()) {
                log.info(String.format("%s(): uuid", name));
            }
            results = Response.status(Status.BAD_REQUEST).build();
        } else if (rto.validate()) {
            // this should be safe since parameters have been validated.
            if (log.isDebugEnabled()) {
                log.debug(String.format("%s(%s): entry", name, Arrays.toString(pjp.getArgs())));
            }
            results = pjp.proceed(pjp.getArgs());
        } else {
            // FIXME: this is unsafe
            if (log.isInfoEnabled()) {
                log.info(String.format("%s(%s): bad arguments", name, Arrays.toString(pjp.getArgs())));
            }
            // TODO: tell caller what the problems were
            results = Response.status(Status.BAD_REQUEST).build();
        }

        return results;
    }

    /**
     * Check post values on delete method. This is actually a no-op but it
     * allows us to log method entry.
     * 
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource) && args(uuid,version) && execution(* *.delete*(..))")
    public Object checkParametersDelete(ProceedingJoinPoint pjp, String uuid, Integer version) throws Throwable {
        final Logger log = Logger.getLogger(pjp.getSignature().getDeclaringType());
        final String name = pjp.getSignature().getName();
        Object results = null;

        if (!StudentUtil.isPossibleUuid(uuid)) {
            // this is a possible attack.
            if (log.isInfoEnabled()) {
                log.info(String.format("%s(): uuid", name));
            }
            results = Response.status(Status.BAD_REQUEST).build();
        } else {
            // this should be safe since parameters have been validated.
            if (log.isDebugEnabled()) {
                log.debug(String.format("%s(%s): entry", name, Arrays.toString(pjp.getArgs())));
            }
            results = pjp.proceed(pjp.getArgs());
        }

        return results;
    }

    /**
     * Check post values on find methods. This is actually a no-op but it allows
     * us to log method entry.
     * 
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource) && execution(* *.find*(..))")
    public Object checkParametersFind(ProceedingJoinPoint pjp) throws Throwable {
        final Logger log = Logger.getLogger(pjp.getSignature().getDeclaringType());

        if (log.isDebugEnabled()) {
            log.debug(String.format("%s(%s): entry", pjp.getSignature().getName(), Arrays.toString(pjp.getArgs())));
        }
        final Object results = pjp.proceed(pjp.getArgs());

        return results;
    }
}

Updated Spring Configuration

We must tell Spring to search for AOP classes. This is a one-line change to our configuration file.

  1. <beans xmlns="http://www.springframework.org/schema/beans"
  2.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3.     xmlns:context="http://www.springframework.org/schema/context"
  4.    xmlns:aop="http://www.springframework.org/schema/aop"
  5.     xsi:schemaLocation="http://www.springframework.org/schema/beans
  6.    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  7.    http://www.springframework.org/schema/context
  8.    http://www.springframework.org/schema/context/spring-context-3.0.xsd
  9.    http://www.springframework.org/schema/aop
  10.    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
  11.  
  12.     <aop:aspectj-autoproxy/>
  13. </beans>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

    <aop:aspectj-autoproxy/>
</beans>

Updated Resource

We can now simplify our Resource classes. Several methods have been reduced to the happy path alone.

  1. @Service
  2. @Path("/course")
  3. public class CourseResource extends AbstractResource {
  4.     private static final Logger LOG = Logger.getLogger(CourseResource.class);
  5.     private static final Course[] EMPTY_COURSE_ARRAY = new Course[0];
  6.  
  7.     @Resource
  8.     private CourseFinderService finder;
  9.  
  10.     @Resource
  11.     private CourseManagerService manager;
  12.  
  13.     @Resource
  14.     private TestRunService testRunService;
  15.  
  16.     /**
  17.      * Default constructor.
  18.      */
  19.     public CourseResource() {
  20.  
  21.     }
  22.  
  23.     /**
  24.      * Set values used in unit tests. (Required due to AOP)
  25.      *
  26.      * @param finder
  27.      * @param manager
  28.      * @param testService
  29.      */
  30.     void setServices(CourseFinderService finder, CourseManagerService manager, TestRunService testRunService) {
  31.         this.finder = finder;
  32.         this.manager = manager;
  33.         this.testRunService = testRunService;
  34.     }
  35.  
  36.     /**
  37.      * Get all Courses.
  38.      *
  39.      * @return
  40.      */
  41.     @GET
  42.     @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  43.     public Response findAllCourses() {
  44.         final List courses = finder.findAllCourses();
  45.  
  46.         final List results = new ArrayList(courses.size());
  47.         for (Course course : courses) {
  48.             results.add(scrubCourse(course));
  49.         }
  50.  
  51.         final Response response = Response.ok(results.toArray(EMPTY_COURSE_ARRAY)).build();
  52.  
  53.         return response;
  54.     }
  55.  
  56.     /**
  57.      * Create a Course.
  58.      *
  59.      * FIXME: what about uniqueness violations?
  60.      *
  61.      * @param req
  62.      * @return
  63.      */
  64.     @POST
  65.     @Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  66.     @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  67.     public Response createCourse(CourseInfo req) {
  68.         final String code = req.getCode();
  69.         final String name = req.getName();
  70.  
  71.         Response response = null;
  72.         Course course = null;
  73.  
  74.         if (req.getTestUuid() != null) {
  75.             TestRun testRun = testRunService.findTestRunByUuid(req.getTestUuid());
  76.             if (testRun != null) {
  77.                 course = manager.createCourseForTesting(code, name, req.getSummary(), req.getDescription(),
  78.                         req.getCreditHours(), testRun);
  79.             } else {
  80.                 response = Response.status(Status.BAD_REQUEST).entity("unknown test UUID").build();
  81.             }
  82.         } else {
  83.             course = manager.createCourse(code, name, req.getSummary(), req.getDescription(), req.getCreditHours());
  84.         }
  85.         if (course == null) {
  86.             response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
  87.         } else {
  88.             response = Response.created(URI.create(course.getUuid())).entity(scrubCourse(course)).build();
  89.         }
  90.  
  91.         return response;
  92.     }
  93.  
  94.     /**
  95.      * Get a specific Course.
  96.      *
  97.      * @param uuid
  98.      * @return
  99.      */
  100.     @Path("/{courseId}")
  101.     @GET
  102.     @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  103.     public Response getCourse(@PathParam("courseId") String id) {
  104.  
  105.         // 'object not found' handled by AOP
  106.         Course course = finder.findCourseByUuid(id);
  107.         final Response response = Response.ok(scrubCourse(course)).build();
  108.  
  109.         return response;
  110.     }
  111.  
  112.     /**
  113.      * Update a Course.
  114.      *
  115.      * FIXME: what about uniqueness violations?
  116.      *
  117.      * @param id
  118.      * @param req
  119.      * @return
  120.      */
  121.     @Path("/{courseId}")
  122.     @POST
  123.     @Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  124.     @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  125.     public Response updateCourse(@PathParam("courseId") String id, CourseInfo req) {
  126.  
  127.         final String name = req.getName();
  128.  
  129.         // 'object not found' handled by AOP
  130.         final Course course = finder.findCourseByUuid(id);
  131.         final Course updatedCourse = manager.updateCourse(course, name, req.getSummary(), req.getDescription(),
  132.                 req.getCreditHours());
  133.         final Response response = Response.ok(scrubCourse(updatedCourse)).build();
  134.  
  135.         return response;
  136.     }
  137.  
  138.     /**
  139.      * Delete a Course.
  140.      *
  141.      * @param id
  142.      * @return
  143.      */
  144.     @Path("/{courseId}")
  145.     @DELETE
  146.     public Response deleteCourse(@PathParam("courseId") String id, @PathParam("version") Integer version) {
  147.  
  148.         // we don't use AOP handler since it's okay for there to be no match
  149.         try {
  150.             manager.deleteCourse(id, version);
  151.         } catch (ObjectNotFoundException exception) {
  152.             LOG.debug("course not found: " + id);
  153.         }
  154.  
  155.         final Response response = Response.noContent().build();
  156.  
  157.         return response;
  158.     }
  159. }
@Service
@Path("/course")
public class CourseResource extends AbstractResource {
    private static final Logger LOG = Logger.getLogger(CourseResource.class);
    private static final Course[] EMPTY_COURSE_ARRAY = new Course[0];

    @Resource
    private CourseFinderService finder;

    @Resource
    private CourseManagerService manager;

    @Resource
    private TestRunService testRunService;

    /**
     * Default constructor.
     */
    public CourseResource() {

    }

    /**
     * Set values used in unit tests. (Required due to AOP)
     * 
     * @param finder
     * @param manager
     * @param testService
     */
    void setServices(CourseFinderService finder, CourseManagerService manager, TestRunService testRunService) {
        this.finder = finder;
        this.manager = manager;
        this.testRunService = testRunService;
    }

    /**
     * Get all Courses.
     * 
     * @return
     */
    @GET
    @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    public Response findAllCourses() {
        final List courses = finder.findAllCourses();

        final List results = new ArrayList(courses.size());
        for (Course course : courses) {
            results.add(scrubCourse(course));
        }

        final Response response = Response.ok(results.toArray(EMPTY_COURSE_ARRAY)).build();

        return response;
    }

    /**
     * Create a Course.
     * 
     * FIXME: what about uniqueness violations?
     * 
     * @param req
     * @return
     */
    @POST
    @Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    public Response createCourse(CourseInfo req) {
        final String code = req.getCode();
        final String name = req.getName();

        Response response = null;
        Course course = null;

        if (req.getTestUuid() != null) {
            TestRun testRun = testRunService.findTestRunByUuid(req.getTestUuid());
            if (testRun != null) {
                course = manager.createCourseForTesting(code, name, req.getSummary(), req.getDescription(),
                        req.getCreditHours(), testRun);
            } else {
                response = Response.status(Status.BAD_REQUEST).entity("unknown test UUID").build();
            }
        } else {
            course = manager.createCourse(code, name, req.getSummary(), req.getDescription(), req.getCreditHours());
        }
        if (course == null) {
            response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
        } else {
            response = Response.created(URI.create(course.getUuid())).entity(scrubCourse(course)).build();
        }

        return response;
    }

    /**
     * Get a specific Course.
     * 
     * @param uuid
     * @return
     */
    @Path("/{courseId}")
    @GET
    @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    public Response getCourse(@PathParam("courseId") String id) {

        // 'object not found' handled by AOP
        Course course = finder.findCourseByUuid(id);
        final Response response = Response.ok(scrubCourse(course)).build();

        return response;
    }

    /**
     * Update a Course.
     * 
     * FIXME: what about uniqueness violations?
     * 
     * @param id
     * @param req
     * @return
     */
    @Path("/{courseId}")
    @POST
    @Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    public Response updateCourse(@PathParam("courseId") String id, CourseInfo req) {

        final String name = req.getName();

        // 'object not found' handled by AOP
        final Course course = finder.findCourseByUuid(id);
        final Course updatedCourse = manager.updateCourse(course, name, req.getSummary(), req.getDescription(),
                req.getCreditHours());
        final Response response = Response.ok(scrubCourse(updatedCourse)).build();

        return response;
    }

    /**
     * Delete a Course.
     * 
     * @param id
     * @return
     */
    @Path("/{courseId}")
    @DELETE
    public Response deleteCourse(@PathParam("courseId") String id, @PathParam("version") Integer version) {

        // we don't use AOP handler since it's okay for there to be no match
        try {
            manager.deleteCourse(id, version);
        } catch (ObjectNotFoundException exception) {
            LOG.debug("course not found: " + id);
        }

        final Response response = Response.noContent().build();

        return response;
    }
}

Unit Tests

The unit tests require a change to every test since we can’t simply instantiate the objects being tested – we must use Spring so the AOP classes are properly weaved. Fortunately that’s essentially the only change – we retrieve the Resource and set the services via a package-private method instead of via package-private constructors.

We also need to create Spring values for our service beans. A Configurer class takes care of this.

  1. @Configuration
  2. @ComponentScan(basePackages = { "com.invariantproperties.sandbox.student.webservice.server.rest" })
  3. @ImportResource({ "classpath:applicationContext-rest.xml" })
  4. // @PropertySource("classpath:application.properties")
  5. public class TestRestApplicationContext1 {
  6.  
  7.     @Bean
  8.     public CourseFinderService courseFinderService() {
  9.         return null;
  10.     }
  11.  
  12.     @Bean
  13.     public CourseManagerService courseManagerService() {
  14.         return null;
  15.     }
  16.  
  17.     ....
@Configuration
@ComponentScan(basePackages = { "com.invariantproperties.sandbox.student.webservice.server.rest" })
@ImportResource({ "classpath:applicationContext-rest.xml" })
// @PropertySource("classpath:application.properties")
public class TestRestApplicationContext1 {

    @Bean
    public CourseFinderService courseFinderService() {
        return null;
    }

    @Bean
    public CourseManagerService courseManagerService() {
        return null;
    }

    ....

Integration Tests

The integration tests do not require any changes.

Source Code

The source code is at https://github.com/beargiles/project-student [github] and http://beargiles.github.io/project-student/ [github pages].

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

Check your REST parameters!

Bear Giles | January 6, 2014

I was doing research related to my ongoing “project student” series and realized that I had made one of the most common – and most easily remedied – mistakes. I wasn’t using everything I know about the webapp to push my security perimeter outwards.

I am thinking specifically about the UUID parameters. I know that every valid externally visible ID will be a UUID. I know the form of the UUIDs. So why don’t I verify that my “uuid” parameters are potentially valid UUIDs before going any further?

It’s true that the database layer won’t recognize a bad “uuid” value – but that may not be the intent of the attacker. Perhaps it’s part of a SQL injection attack. Perhaps it’s part of an XSS attack. Perhaps it’s part of an attack on my logs (e.g., by including a really long value that might cause a buffer overflow). Perhaps it’s part of something I’ve never heard of. It doesn’t matter – I will always be stronger by eliminating known-invalid data as quickly as possible.

Utility Method

The utility method to determine whether a value is a possible UUID uses a simple regex pattern.

  1. public final class StudentUtil {
  2.     private static final Pattern UUID_PATTERN = Pattern
  3.             .compile("^\\p{XDigit}{8}+-\\p{XDigit}{4}+-\\p{XDigit}{4}-\\p{XDigit}{4}+-\\p{XDigit}{12}$");
  4.  
  5.     /**
  6.      * Private constructor to prevent instantiation.
  7.      */
  8.     private StudentUtil() {
  9.  
  10.     }
  11.  
  12.     public static boolean isPossibleUuid(String value) {
  13.         return value != null && UUID_PATTERN.matcher(value).matches();
  14.     }
  15. }
public final class StudentUtil {
    private static final Pattern UUID_PATTERN = Pattern
            .compile("^\\p{XDigit}{8}+-\\p{XDigit}{4}+-\\p{XDigit}{4}-\\p{XDigit}{4}+-\\p{XDigit}{12}$");

    /**
     * Private constructor to prevent instantiation.
     */
    private StudentUtil() {

    }

    public static boolean isPossibleUuid(String value) {
        return value != null && UUID_PATTERN.matcher(value).matches();
    }
}

If we want to be aggressive we could carefully select our UUIDs so they have additional properties that we can check. For instance the corresponding BigInteger could always have a remainder of 3 mod 17. It’s unlikely an attack would know this and we would have warning when somebody is probing our system. An even more sophisticated approach would use a different property for each class of UUID, e.g., a ‘course’ UUID might be 3 mod 17 while a ‘student’ UUID is 5 mod 17.

Unit Test

It’s easy to go overboard on our tests but a minimal set would be checking for non-hex digits,
too many or too few values, an empty string, and a null value.

  1. public class StudentUtilTest {
  2.  
  3.     @Test
  4.     public void testValidUuid() {
  5.         assertTrue(StudentUtil.isPossibleUuid("63c7d688-705c-4374-937c-6628952b41e1"));
  6.     }
  7.  
  8.     @Test
  9.     public void testInvalidUuid() {
  10.         assertTrue(!StudentUtil.isPossibleUuid("63c7d68x-705c-4374-937c-6628952b41e1"));
  11.         assertTrue(!StudentUtil.isPossibleUuid("63c7d68-8705c-4374-937c-6628952b41e1"));
  12.         assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c4-374-937c-6628952b41e1"));
  13.         assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c-43749-37c-6628952b41e1"));
  14.         assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c-4374-937c6-628952b41e1"));
  15.         assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c-4374-937c-6628952b41e1a"));
  16.         assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c-4374-937c-6628952b41e"));
  17.         assertTrue(!StudentUtil.isPossibleUuid(""));
  18.         assertTrue(!StudentUtil.isPossibleUuid(null));
  19.     }
  20. }
public class StudentUtilTest {

    @Test
    public void testValidUuid() {
        assertTrue(StudentUtil.isPossibleUuid("63c7d688-705c-4374-937c-6628952b41e1"));
    }

    @Test
    public void testInvalidUuid() {
        assertTrue(!StudentUtil.isPossibleUuid("63c7d68x-705c-4374-937c-6628952b41e1"));
        assertTrue(!StudentUtil.isPossibleUuid("63c7d68-8705c-4374-937c-6628952b41e1"));
        assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c4-374-937c-6628952b41e1"));
        assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c-43749-37c-6628952b41e1"));
        assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c-4374-937c6-628952b41e1"));
        assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c-4374-937c-6628952b41e1a"));
        assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c-4374-937c-6628952b41e"));
        assertTrue(!StudentUtil.isPossibleUuid(""));
        assertTrue(!StudentUtil.isPossibleUuid(null));
    }
}

REST Server

The REST server should check the UUID value for all methods that require one. It’s safe to log the request parameter after we’ve verified it’s a well-formed UUID but still need to be careful about logging unsanitized values in the request.

  1.     @Path("/{courseId}")
  2.     @GET
  3.     @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  4.     public Response getCourse(@PathParam("courseId") String id) {
  5.  
  6.         Response response = null;
  7.         if (!StudentUtil.isPossibleUuid(id)) {
  8.             response = Response.status(Status.BAD_REQUEST).build();
  9.             LOG.info("attempt to use malformed UUID");
  10.         } else {
  11.             LOG.debug("CourseResource: getCourse(" + id + ")");
  12.             try {
  13.                 Course course = finder.findCourseByUuid(id);
  14.                 response = Response.ok(scrubCourse(course)).build();
  15.             } catch (ObjectNotFoundException e) {
  16.                 response = Response.status(Status.NOT_FOUND).build();
  17.                 LOG.debug("course not found: " + id);
  18.             } catch (Exception e) {
  19.                 if (!(e instanceof UnitTestException)) {
  20.                     LOG.info("unhandled exception", e);
  21.                 }
  22.                 response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
  23.             }
  24.         }
  25.  
  26.         return response;
  27.     }
    @Path("/{courseId}")
    @GET
    @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    public Response getCourse(@PathParam("courseId") String id) {

        Response response = null;
        if (!StudentUtil.isPossibleUuid(id)) {
            response = Response.status(Status.BAD_REQUEST).build();
            LOG.info("attempt to use malformed UUID");
        } else {
            LOG.debug("CourseResource: getCourse(" + id + ")");
            try {
                Course course = finder.findCourseByUuid(id);
                response = Response.ok(scrubCourse(course)).build();
            } catch (ObjectNotFoundException e) {
                response = Response.status(Status.NOT_FOUND).build();
                LOG.debug("course not found: " + id);
            } catch (Exception e) {
                if (!(e instanceof UnitTestException)) {
                    LOG.info("unhandled exception", e);
                }
                response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
            }
        }

        return response;
    }

An obvious improvement is to move this check (and the exception catchall) into an AOP wrapper to all service methods. This will simplify the code and go a long way towards guaranteeing that the checks are always performed. (I’m not using it in Project Student at the moment since the webservice server layer doesn’t currently have Spring dependencies.)

You can make a strong opsec argument that the REST methods should return a NOT_FOUND response instead of a BAD_REQUEST response in order to reduce information leakage.

Webapps

The details differ but we should do the same thing with webapps even if they’re just shallow front-ends to REST services. Whenever there’s a UUID, no matter it’s source, it should be checked before it is used.

Filters

There is a school of thought that security should be handled separately from the application – that the best security is knit in at deployment (via filters and AOP) instead of being baked into the application. Nobody is suggesting that app developers should ignore security considerations, just that checks like I discussed above are clutter that distract the developer and aren’t reliable since it’s easy for a developer to overlook. They would recommend using AOP or a filter instead.

It’s straightforward to write a filter that does the same work as the code above:

  1. public class RestParameterFilter implements Filter {
  2.     private static final Logger LOG = Logger.getLogger(RestParameterFilter.class);
  3.     private static final Set<String> validNouns = new HashSet<>();
  4.  
  5.     /**
  6.      * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
  7.      */
  8.     @Override
  9.     public void init(FilterConfig cfg) throws ServletException {
  10.  
  11.         // learn valid nouns
  12.         final String nouns = cfg.getInitParameter("valid-nouns");
  13.         if (nouns != null) {
  14.             for (String noun : nouns.split(",")) {
  15.                 validNouns.add(noun.trim());
  16.             }
  17.         }
  18.     }
  19.  
  20.     /**
  21.      * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
  22.      *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
  23.      */
  24.     @Override
  25.     public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException,
  26.             ServletException {
  27.  
  28.         HttpServletRequest hreq = (HttpServletRequest) req;
  29.         HttpServletResponse hresp = (HttpServletResponse) resp;
  30.  
  31.         // verify the noun + uuid
  32.         if (!checkPathInfo(hreq, hresp)) {
  33.             return;
  34.         }
  35.  
  36.         // do additional tests, e.g., inspect payload
  37.  
  38.         chain.doFilter(req, resp);
  39.     }
  40.  
  41.     /**
  42.      * @see javax.servlet.Filter#destroy()
  43.      */
  44.     @Override
  45.     public void destroy() {
  46.     }
  47.  
  48.     /**
  49.      * Check the pathInfo. We know that all paths should have the form
  50.      * /{noun}/{uuid}/...
  51.      *
  52.      * @param req
  53.      * @return
  54.      */
  55.     public boolean checkPathInfo(HttpServletRequest req, HttpServletResponse resp) {
  56.         // this pattern only handles noun and UUID, no additional parameters.
  57.         Pattern pattern = Pattern.compile("^/([\\p{Alpha}]+)(/?([\\p{XDigit}-]+)?)?");
  58.         Matcher matcher = pattern.matcher(req.getPathInfo());
  59.         matcher.find();
  60.  
  61.         // verify this is a valid noun.
  62.         if ((matcher.groupCount() >= 1) && !validNouns.contains(matcher.group(1))) {
  63.             // LOG.info("unrecognized noun");
  64.             LOG.info("unrecognized noun: '" + matcher.group(1) + "'");
  65.             resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
  66.             return false;
  67.         }
  68.  
  69.         // verify this is a valid verb.
  70.         if ((matcher.groupCount() >= 4) && !StudentUtil.isPossibleUuid(matcher.group(4))) {
  71.             LOG.info("invalid UUID");
  72.             resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
  73.             return false;
  74.         }
  75.  
  76.         return true;
  77.     }
  78. }
public class RestParameterFilter implements Filter {
    private static final Logger LOG = Logger.getLogger(RestParameterFilter.class);
    private static final Set<String> validNouns = new HashSet<>();

    /**
     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
     */
    @Override
    public void init(FilterConfig cfg) throws ServletException {

        // learn valid nouns
        final String nouns = cfg.getInitParameter("valid-nouns");
        if (nouns != null) {
            for (String noun : nouns.split(",")) {
                validNouns.add(noun.trim());
            }
        }
    }

    /**
     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
     *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
     */
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException,
            ServletException {

        HttpServletRequest hreq = (HttpServletRequest) req;
        HttpServletResponse hresp = (HttpServletResponse) resp;

        // verify the noun + uuid
        if (!checkPathInfo(hreq, hresp)) {
            return;
        }

        // do additional tests, e.g., inspect payload

        chain.doFilter(req, resp);
    }

    /**
     * @see javax.servlet.Filter#destroy()
     */
    @Override
    public void destroy() {
    }

    /**
     * Check the pathInfo. We know that all paths should have the form
     * /{noun}/{uuid}/...
     * 
     * @param req
     * @return
     */
    public boolean checkPathInfo(HttpServletRequest req, HttpServletResponse resp) {
        // this pattern only handles noun and UUID, no additional parameters.
        Pattern pattern = Pattern.compile("^/([\\p{Alpha}]+)(/?([\\p{XDigit}-]+)?)?");
        Matcher matcher = pattern.matcher(req.getPathInfo());
        matcher.find();

        // verify this is a valid noun.
        if ((matcher.groupCount() >= 1) && !validNouns.contains(matcher.group(1))) {
            // LOG.info("unrecognized noun");
            LOG.info("unrecognized noun: '" + matcher.group(1) + "'");
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return false;
        }

        // verify this is a valid verb.
        if ((matcher.groupCount() >= 4) && !StudentUtil.isPossibleUuid(matcher.group(4))) {
            LOG.info("invalid UUID");
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return false;
        }

        return true;
    }
}

There’s no reason why we can’t also inspect the payload. For instance we can verify that dates, phone numbers and credit card numbers are properly formed; or that names only include letters (including non-Latin characters like ñ), spaces and apostrophes. (Think “Anne-Marie Peña O’Brien”.) It’s important to remember that these checks are not for ‘valid’ data – it’s to eliminate clearly ‘invalid’ data.

We must add the filter to our web.xml file.

web.xml

  1. <filter>
  2.     <filter-name>REST parameter filter</filter-name>
  3.     <filter-class>com.invariantproperties.sandbox.student.webservice.security.RestParameterFilter</filter-class>
  4.      <init-param>
  5.         <param-name>valid-nouns</param-name>
  6.         <param-value>classroom,course,instructor,section,student,term,testRun</param-value>
  7.     </init-param>
  8. </filter>
  9.  
  10. <filter-mapping>
  11.     <filter-name>REST parameter filter</filter-name>
  12.     <servlet-name>REST dispatcher</servlet-name>
  13. </filter-mapping>
<filter>
    <filter-name>REST parameter filter</filter-name>
    <filter-class>com.invariantproperties.sandbox.student.webservice.security.RestParameterFilter</filter-class>
     <init-param>
        <param-name>valid-nouns</param-name>
        <param-value>classroom,course,instructor,section,student,term,testRun</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>REST parameter filter</filter-name>
    <servlet-name>REST dispatcher</servlet-name>
</filter-mapping>

ModSecurity

It’s easy to write a filter for simple elements like phone numbers and names but plaintext fields are another matter. These fields need the maximum flexibility while at the same time we want to minimize the risk of XSS and other attacks.

A good resource along these lines is ModSecurity. This was originally an Apache module but it has been adopted by Trustwave Spider Labs. It sits on the web server – not the webapp – and inspects the data crossing it. A recent port (in summer 2013) allows it to be set up using a servlet filter instead of an external reverse proxy. (It uses JNI to instrument the containing appserver.)

For more information see ModSecurity for Java.

Comments
No Comments »
Categories
java, security
Comments rss Comments rss
Trackback Trackback

Project Student: Maintenance Webapp (editable)

Bear Giles | January 4, 2014

This is part of Project Student. Other posts are Webservice Client With Jersey, Webservice Server with Jersey, Business Layer, Persistence with Spring Data, Sharding Integration Test Data, Webservice Integration, JPA Criteria Queries and Maintenance Webapp (read-only).

Last time we created a simple webapp that allows us to take a quick peek into the database. It had very limited functionality – the primary goal was to knit together a system that exercised the entire stack from web browser to database. This time we add actual CRUD support.

This post is borrows heavily from the jumpstart site but there are significant differences. There’s a lot of code but it’s boilerplate that can be easily reused.

Limitations

User authentication – no effort has been made to authenticate users.

Encryption – no effort has been made to encrypt communications.

Pagination – no effort has been made to support pagination. The Tapestry 5 component will give the appearance of pagination but it will always contain the same first page of data.

Error Messages – error messages will be shown but server-side errors will be uninformative for now.

Cross-Site Scripting (XSS) – no effort has been made to prevent XSS attacks.

Internationalization – no effort has been made to support internationalization.

Goal

We want the standard CRUD pages.

First, we need to be able to create a new course. Our list of courses should include a link as a default message when we don’t have any data. (The first “create…” is a separate element.)

Course List - empty list

Now a creation page with several fields. A code uniquely identifies a course, e.g., CSCI 101, and name, summary and description should be self-explanatory.

Course Editor - create

After successful creation we’re taken to a review page.

Course Editor - view

And then back to an update page if we need to make a change.

Course Editor -update

At any point we can go back to the list page.

Course List - list

We’re prompted for confirmation before deleting a record.

Course List - delete

And finally we’re able to show server-side errors, e.g., for duplicate values in unique fields, even if the messages are pretty useless at the moment.

Course Editor - create error

We also have client-side error checking although I don’t show it here.

Index.tml

We start with the index page. It’s similar to what we saw in the last post.

Tapestry 5 has three major types of links. A pagelink is mapped to a standard HTML link. An actionlink is directly handled by the corresponding class, e.g., Index.java for the Index.tml template. Finally an eventlink injects an event into the normal event flow within the tapestry engine. All of my links go to a closely related page so I use an actionlink.

  1. <html t:type="layout" title="Course List"
  2.      t:sidebarTitle="Framework Version"
  3.      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
  4.      xmlns:p="tapestry:parameter">
  5.  
  6.     <t:zone t:id="zone">  
  7.         <p>
  8.             "Course" page
  9.         </p>
  10.  
  11.         <t:actionlink t:id="create">Create...</t:actionlink><br/>
  12.  
  13.         <t:grid source="courses" row="course" include="code, name,creationdate" add="edit,delete">
  14.             <p:codecell>
  15.                 <t:actionlink t:id="view" context="course.uuid">${course.code}</t:actionlink>
  16.             </p:codecell>
  17.             <p:editcell>
  18.                 <t:actionlink t:id="update" context="course.uuid">Edit</t:actionlink>
  19.             </p:editcell>
  20.             <p:deletecell>
  21.                 <t:actionlink t:id="delete" context="course.uuid" t:mixins="Confirm" t:message="Delete ${course.name}?">Delete</t:actionlink>
  22.             </p:deletecell>
  23.             <p:empty>
  24.               <p>There are no courses to display; you can <t:actionlink t:id="create1">create</t:actionlink> one.</p>
  25.             </p:empty>
  26.         </t:grid>
  27.     </t:zone>
  28.  
  29.     <p:sidebar>
  30.         <p>
  31.             [
  32.             <t:pagelink page="Index">Index</t:pagelink>
  33.             ]<br/>
  34.             [
  35.             <t:pagelink page="Course/Index">Courses</t:pagelink>
  36.             ]
  37.         </p>
  38.     </p:sidebar>
  39.  
  40. </html>
<html t:type="layout" title="Course List"
      t:sidebarTitle="Framework Version"
      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
      xmlns:p="tapestry:parameter">

    <t:zone t:id="zone">   
        <p>
            "Course" page
        </p>

        <t:actionlink t:id="create">Create...</t:actionlink><br/>
 
        <t:grid source="courses" row="course" include="code, name,creationdate" add="edit,delete">
            <p:codecell>
                <t:actionlink t:id="view" context="course.uuid">${course.code}</t:actionlink>
            </p:codecell>
            <p:editcell>
                <t:actionlink t:id="update" context="course.uuid">Edit</t:actionlink>
            </p:editcell>
            <p:deletecell>
                <t:actionlink t:id="delete" context="course.uuid" t:mixins="Confirm" t:message="Delete ${course.name}?">Delete</t:actionlink>
            </p:deletecell>
            <p:empty>
              <p>There are no courses to display; you can <t:actionlink t:id="create1">create</t:actionlink> one.</p>
            </p:empty>
        </t:grid>
    </t:zone>

    <p:sidebar>
        <p>
            [
            <t:pagelink page="Index">Index</t:pagelink>
            ]<br/>
            [
            <t:pagelink page="Course/Index">Courses</t:pagelink>
            ]
        </p>
    </p:sidebar>

</html>

Confirm mixin

The Index.tml template included a ‘mixin’ on the delete actionlink. It uses a mixture of javascript and java to display a popup message to ask the user to verify that he wants to delete the course.

This code is straight from the jumpstart and Tapestry sites.

  1. // from http://jumpstart.doublenegative.com.au/
  2. Confirm = Class.create({
  3.        
  4.     initialize: function(elementId, message) {
  5.         this.message = message;
  6.         Event.observe($(elementId), 'click', this.doConfirm.bindAsEventListener(this));
  7.     },
  8.    
  9.     doConfirm: function(e) {
  10.        
  11.         // Pop up a javascript Confirm Box (see http://www.w3schools.com/js/js_popup.asp)
  12.        
  13.         if (!confirm(this.message)) {
  14.                 e.stop();
  15.         }
  16.     }
  17. })
  18.  
  19. // Extend the Tapestry.Initializer with a static method that instantiates a Confirm.
  20.  
  21. Tapestry.Initializer.confirm = function(spec) {
  22.     new Confirm(spec.elementId, spec.message);
  23. }
// from http://jumpstart.doublenegative.com.au/
Confirm = Class.create({
        
    initialize: function(elementId, message) {
        this.message = message;
        Event.observe($(elementId), 'click', this.doConfirm.bindAsEventListener(this));
    },
    
    doConfirm: function(e) {
        
        // Pop up a javascript Confirm Box (see http://www.w3schools.com/js/js_popup.asp)
        
        if (!confirm(this.message)) {
                e.stop();
        }
    }
})

// Extend the Tapestry.Initializer with a static method that instantiates a Confirm.

Tapestry.Initializer.confirm = function(spec) {
    new Confirm(spec.elementId, spec.message);
}

The corresponding java code is

  1. // from http://jumpstart.doublenegative.com.au/
  2. @Import(library = "confirm.js")
  3. public class Confirm {
  4.  
  5.     @Parameter(name = "message", value = "Are you sure?", defaultPrefix = BindingConstants.LITERAL)
  6.     private String message;
  7.  
  8.     @Inject
  9.     private JavaScriptSupport javaScriptSupport;
  10.  
  11.     @InjectContainer
  12.     private ClientElement clientElement;
  13.  
  14.     @AfterRender
  15.     public void afterRender() {
  16.  
  17.         // Tell the Tapestry.Initializer to do the initializing of a Confirm,
  18.         // which it will do when the DOM has been
  19.         // fully loaded.
  20.  
  21.         JSONObject spec = new JSONObject();
  22.         spec.put("elementId", clientElement.getClientId());
  23.         spec.put("message", message);
  24.         javaScriptSupport.addInitializerCall("confirm", spec);
  25.     }
  26. }
// from http://jumpstart.doublenegative.com.au/
@Import(library = "confirm.js")
public class Confirm {

    @Parameter(name = "message", value = "Are you sure?", defaultPrefix = BindingConstants.LITERAL)
    private String message;

    @Inject
    private JavaScriptSupport javaScriptSupport;

    @InjectContainer
    private ClientElement clientElement;

    @AfterRender
    public void afterRender() {

        // Tell the Tapestry.Initializer to do the initializing of a Confirm,
        // which it will do when the DOM has been
        // fully loaded.

        JSONObject spec = new JSONObject();
        spec.put("elementId", clientElement.getClientId());
        spec.put("message", message);
        javaScriptSupport.addInitializerCall("confirm", spec);
    }
}

Index.java

The java that supports the index template is straightforward since we only need to define a data source and provide a few action handlers.

  1. package com.invariantproperties.sandbox.student.maintenance.web.pages.course;
  2.  
  3. public class Index {
  4.     @Property
  5.     @Inject
  6.     @Symbol(SymbolConstants.TAPESTRY_VERSION)
  7.     private String tapestryVersion;
  8.  
  9.     @InjectComponent
  10.     private Zone zone;
  11.  
  12.     @Inject
  13.     private CourseFinderService courseFinderService;
  14.  
  15.     @Inject
  16.     private CourseManagerService courseManagerService;
  17.  
  18.     @Property
  19.     private Course course;
  20.  
  21.     // our sibling page
  22.     @InjectPage
  23.     private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Editor editorPage;
  24.  
  25.     /**
  26.      * Get the datasource containing our data.
  27.      *
  28.      * @return
  29.      */
  30.     public GridDataSource getCourses() {
  31.         return new CoursePagedDataSource(courseFinderService);
  32.     }
  33.  
  34.     /**
  35.      * Handle a delete request. This could fail, e.g., if the course has already
  36.      * been deleted.
  37.      *
  38.      * @param courseUuid
  39.      */
  40.     void onActionFromDelete(String courseUuid) {
  41.         courseManagerService.deleteCourse(courseUuid, 0);
  42.     }
  43.  
  44.     /**
  45.      * Bring up editor page in create mode.
  46.      *
  47.      * @param courseUuid
  48.      * @return
  49.      */
  50.     Object onActionFromCreate() {
  51.         editorPage.setup(Mode.CREATE, null);
  52.         return editorPage;
  53.     }
  54.  
  55.     /**
  56.      * Bring up editor page in create mode.
  57.      *
  58.      * @param courseUuid
  59.      * @return
  60.      */
  61.     Object onActionFromCreate1() {
  62.         return onActionFromCreate();
  63.     }
  64.  
  65.     /**
  66.      * Bring up editor page in review mode.
  67.      *
  68.      * @param courseUuid
  69.      * @return
  70.      */
  71.     Object onActionFromView(String courseUuid) {
  72.         editorPage.setup(Mode.REVIEW, courseUuid);
  73.         return editorPage;
  74.     }
  75.  
  76.     /**
  77.      * Bring up editor page in update mode.
  78.      *
  79.      * @param courseUuid
  80.      * @return
  81.      */
  82.     Object onActionFromUpdate(String courseUuid) {
  83.         editorPage.setup(Mode.UPDATE, courseUuid);
  84.         return editorPage;
  85.     }
  86. }
package com.invariantproperties.sandbox.student.maintenance.web.pages.course;

public class Index {
    @Property
    @Inject
    @Symbol(SymbolConstants.TAPESTRY_VERSION)
    private String tapestryVersion;

    @InjectComponent
    private Zone zone;

    @Inject
    private CourseFinderService courseFinderService;

    @Inject
    private CourseManagerService courseManagerService;

    @Property
    private Course course;

    // our sibling page
    @InjectPage
    private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Editor editorPage;

    /**
     * Get the datasource containing our data.
     * 
     * @return
     */
    public GridDataSource getCourses() {
        return new CoursePagedDataSource(courseFinderService);
    }

    /**
     * Handle a delete request. This could fail, e.g., if the course has already
     * been deleted.
     * 
     * @param courseUuid
     */
    void onActionFromDelete(String courseUuid) {
        courseManagerService.deleteCourse(courseUuid, 0);
    }

    /**
     * Bring up editor page in create mode.
     * 
     * @param courseUuid
     * @return
     */
    Object onActionFromCreate() {
        editorPage.setup(Mode.CREATE, null);
        return editorPage;
    }

    /**
     * Bring up editor page in create mode.
     * 
     * @param courseUuid
     * @return
     */
    Object onActionFromCreate1() {
        return onActionFromCreate();
    }

    /**
     * Bring up editor page in review mode.
     * 
     * @param courseUuid
     * @return
     */
    Object onActionFromView(String courseUuid) {
        editorPage.setup(Mode.REVIEW, courseUuid);
        return editorPage;
    }

    /**
     * Bring up editor page in update mode.
     * 
     * @param courseUuid
     * @return
     */
    Object onActionFromUpdate(String courseUuid) {
        editorPage.setup(Mode.UPDATE, courseUuid);
        return editorPage;
    }
}

Editor.tml

The CRUD pages could be three separate pages (for create, review and update) or a single page. I’m following the pattern used by the jumpstart site – a single page. I’ll be honest – I’m not sure why he made this decision – perhaps it is because the pages are closely related and he uses event processing? In any case I’ll discuss the elements separately.

CREATE template

The “create” template is a simple form. You can see that HTML <input> elements are enhanced with some tapestry-specific attributes, plus a few additional tags like <t:errors/> and <t:submit>.

The CustomForm and CustomError are local extensions to the standard Tapestry Form and Error classes. They’re currently empty but allow us to easily add local extensions.

  1. <html t:type="layout" title="Course Editor"
  2.      t:sidebarTitle="Framework Version"
  3.      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter">
  4.  
  5.     <t:zone t:id="zone">  
  6.  
  7.     <t:if test="modeCreate">
  8.         <h1>Create</h1>
  9.        
  10.         <form t:type="form" t:id="createForm" >
  11.             <t:errors/>
  12.    
  13.             <table>
  14.                 <tr>
  15.                     <th><t:label for="code"/>:</th>
  16.                     <td><input t:type="TextField" t:id="code" value="course.code" t:validate="required, maxlength=12" size="12"/></td>
  17.                     <td>(required)</td>
  18.                 </tr>
  19.                 <tr class="err">
  20.                     <th></th>
  21.                     <td colspan="2"><t:CustomError for="code"/></td>
  22.                 </tr>
  23.                 <tr>
  24.                     <th><t:label for="name"/>:</th>
  25.                     <td><input t:type="TextField" t:id="name" value="course.name" t:validate="required, maxlength=80" size="45"/></td>
  26.                     <td>(required)</td>
  27.                 </tr>
  28.                 <tr class="err">
  29.                     <th></th>
  30.                     <td colspan="2"><t:CustomError for="name"/></td>
  31.                 </tr>
  32.                 <tr>
  33.                     <th><t:label for="summary"/>:</th>
  34.                     <td><input cols="50" rows="4" t:type="TextArea" t:id="summary" value="course.summary" t:validate="maxlength=400"/></td>
  35.                 </tr>
  36.                 <tr class="err">
  37.                     <th></th>
  38.                     <td colspan="2"><t:CustomError for="summary"/></td>
  39.                 </tr>
  40.                 <tr>
  41.                     <th><t:label for="description"/>:</th>
  42.                     <td><input cols="50" rows="12" t:type="TextArea" t:id="description" value="course.description" t:validate="maxlength=2000"/></td>
  43.                 </tr>
  44.                 <tr class="err">
  45.                     <th></th>
  46.                     <td colspan="2"><t:CustomError for="description"/></td>
  47.                 </tr>
  48.             </table>
  49.  
  50.             <div class="buttons">
  51.                 <t:submit t:event="cancelCreate" t:context="course.uuid" value="Cancel"/>
  52.                 <input type="submit" value="Save"/>
  53.             </div>
  54.         </form>
  55.     </t:if>
  56.  
  57.     ...
  58. </html>
<html t:type="layout" title="Course Editor"
      t:sidebarTitle="Framework Version"
      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter">

    <t:zone t:id="zone">   

    <t:if test="modeCreate">
        <h1>Create</h1>
        
        <form t:type="form" t:id="createForm" >
            <t:errors/>
    
            <table>
                <tr>
                    <th><t:label for="code"/>:</th>
                    <td><input t:type="TextField" t:id="code" value="course.code" t:validate="required, maxlength=12" size="12"/></td>
                    <td>(required)</td>
                </tr>
                <tr class="err">
                    <th></th>
                    <td colspan="2"><t:CustomError for="code"/></td>
                </tr>
                <tr>
                    <th><t:label for="name"/>:</th>
                    <td><input t:type="TextField" t:id="name" value="course.name" t:validate="required, maxlength=80" size="45"/></td>
                    <td>(required)</td>
                </tr>
                <tr class="err">
                    <th></th>
                    <td colspan="2"><t:CustomError for="name"/></td>
                </tr>
                <tr>
                    <th><t:label for="summary"/>:</th>
                    <td><input cols="50" rows="4" t:type="TextArea" t:id="summary" value="course.summary" t:validate="maxlength=400"/></td>
                </tr>
                <tr class="err">
                    <th></th>
                    <td colspan="2"><t:CustomError for="summary"/></td>
                </tr>
                <tr>
                    <th><t:label for="description"/>:</th>
                    <td><input cols="50" rows="12" t:type="TextArea" t:id="description" value="course.description" t:validate="maxlength=2000"/></td>
                </tr>
                <tr class="err">
                    <th></th>
                    <td colspan="2"><t:CustomError for="description"/></td>
                </tr>
            </table>

            <div class="buttons">
                <t:submit t:event="cancelCreate" t:context="course.uuid" value="Cancel"/>
                <input type="submit" value="Save"/>
            </div>
        </form>
    </t:if>

    ...
</html>

CREATE java

The corresponding java class is straightforward. We must define a few custom events.

The ActivationRequestParameter values are pulled from the URL query string.

The course field contains the values to be used when creating a new object.

The courseForm field contains the corresponding <form> on the template.

The indexPage contains a reference to the index page.

There are four event handlers named onEventFromCreateForm, where event
can be prepare, validate, success or failure. Each event handler has very specific roles.

There is one additional event handler, onCancelCreate(). You can see the name of that event in the <t:submit> tag in the template.

  1. /**
  2.  * This component will trigger the following events on its container (which in
  3.  * this example is the page):
  4.  * {@link Editor.web.components.examples.component.crud.Editor#CANCEL_CREATE} ,
  5.  * {@link Editor.web.components.examples.component.crud.Editor#SUCCESSFUL_CREATE}
  6.  * (Long courseUuid),
  7.  * {@link Editor.web.components.examples.component.crud.Editor#FAILED_CREATE} ,
  8.  */
  9. // @Events is applied to a component solely to document what events it may
  10. // trigger. It is not checked at runtime.
  11. @Events({ Editor.CANCEL_CREATE, Editor.SUCCESSFUL_CREATE, Editor.FAILED_CREATE })
  12. public class Editor {
  13.     public static final String CANCEL_CREATE = "cancelCreate";
  14.     public static final String SUCCESSFUL_CREATE = "successfulCreate";
  15.     public static final String FAILED_CREATE = "failedCreate";
  16.  
  17.     public enum Mode {
  18.         CREATE, REVIEW, UPDATE;
  19.     }
  20.  
  21.     // Parameters
  22.  
  23.     @ActivationRequestParameter
  24.     @Property
  25.     private Mode mode;
  26.  
  27.     @ActivationRequestParameter
  28.     @Property
  29.     private String courseUuid;
  30.  
  31.     // Screen fields
  32.  
  33.     @Property
  34.     private Course course;
  35.  
  36.     // Work fields
  37.  
  38.     // This carries version through the redirect that follows a server-side
  39.     // validation failure.
  40.     @Persist(PersistenceConstants.FLASH)
  41.     private Integer versionFlash;
  42.  
  43.     // Generally useful bits and pieces
  44.  
  45.     @Inject
  46.     private CourseFinderService courseFinderService;
  47.  
  48.     @Inject
  49.     private CourseManagerService courseManagerService;
  50.  
  51.     @Component
  52.     private CustomForm createForm;
  53.  
  54.     @Inject
  55.     private ComponentResources componentResources;
  56.  
  57.     @InjectPage
  58.     private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage;
  59.  
  60.     // The code
  61.  
  62.     public void setup(Mode mode, String courseUuid) {
  63.         this.mode = mode;
  64.         this.courseUuid = courseUuid;
  65.     }
  66.  
  67.     // setupRender() is called by Tapestry right before it starts rendering the
  68.     // component.
  69.  
  70.     void setupRender() {
  71.  
  72.         if (mode == Mode.REVIEW) {
  73.             if (courseUuid == null) {
  74.                 course = null;
  75.                 // Handle null course in the template.
  76.             } else {
  77.                 if (course == null) {
  78.                     try {
  79.                         course = courseFinderService.findCourseByUuid(courseUuid);
  80.                     } catch (ObjectNotFoundException e) {
  81.                         // Handle null course in the template.
  82.                     }
  83.                 }
  84.             }
  85.         }
  86.     }
  87.  
  88.     // /////////////////////////////////////////////////////////////////////
  89.     // CREATE
  90.     // /////////////////////////////////////////////////////////////////////
  91.  
  92.     // Handle event "cancelCreate"
  93.  
  94.     Object onCancelCreate() {
  95.         return indexPage;
  96.     }
  97.  
  98.     // Component "createForm" bubbles up the PREPARE event when it is rendered
  99.     // or submitted
  100.  
  101.     void onPrepareFromCreateForm() throws Exception {
  102.         // Instantiate a Course for the form data to overlay.
  103.         course = new Course();
  104.     }
  105.  
  106.     // Component "createForm" bubbles up the VALIDATE event when it is submitted
  107.  
  108.     void onValidateFromCreateForm() {
  109.  
  110.         if (createForm.getHasErrors()) {
  111.             // We get here only if a server-side validator detected an error.
  112.             return;
  113.         }
  114.  
  115.         try {
  116.             course = courseManagerService.createCourse(course.getCode(), course.getName(), course.getSummary(),
  117.                     course.getDescription(), 1);
  118.         } catch (RestClientFailureException e) {
  119.             createForm.recordError("Internal error on server.");
  120.             createForm.recordError(e.getMessage());
  121.         } catch (Exception e) {
  122.             createForm.recordError(ExceptionUtil.getRootCauseMessage(e));
  123.         }
  124.     }
  125.  
  126.     // Component "createForm" bubbles up SUCCESS or FAILURE when it is
  127.     // submitted, depending on whether VALIDATE
  128.     // records an error
  129.  
  130.     boolean onSuccessFromCreateForm() {
  131.         componentResources.triggerEvent(SUCCESSFUL_CREATE, new Object[] { course.getUuid() }, null);
  132.  
  133.         // We don't want "success" to bubble up, so we return true to say we've
  134.         // handled it.
  135.         mode = Mode.REVIEW;
  136.         courseUuid = course.getUuid();
  137.         return true;
  138.     }
  139.  
  140.     boolean onFailureFromCreateForm() {
  141.         // Rather than letting "failure" bubble up which doesn't say what you
  142.         // were trying to do, we trigger new event
  143.         // "failedCreate". It will bubble up because we don't have a handler
  144.         // method for it.
  145.         componentResources.triggerEvent(FAILED_CREATE, null, null);
  146.  
  147.         // We don't want "failure" to bubble up, so we return true to say we've
  148.         // handled it.
  149.         return true;
  150.     }
  151.  
  152.     ....
  153. }
/**
 * This component will trigger the following events on its container (which in
 * this example is the page):
 * {@link Editor.web.components.examples.component.crud.Editor#CANCEL_CREATE} ,
 * {@link Editor.web.components.examples.component.crud.Editor#SUCCESSFUL_CREATE}
 * (Long courseUuid),
 * {@link Editor.web.components.examples.component.crud.Editor#FAILED_CREATE} ,
 */
// @Events is applied to a component solely to document what events it may
// trigger. It is not checked at runtime.
@Events({ Editor.CANCEL_CREATE, Editor.SUCCESSFUL_CREATE, Editor.FAILED_CREATE })
public class Editor {
    public static final String CANCEL_CREATE = "cancelCreate";
    public static final String SUCCESSFUL_CREATE = "successfulCreate";
    public static final String FAILED_CREATE = "failedCreate";

    public enum Mode {
        CREATE, REVIEW, UPDATE;
    }

    // Parameters

    @ActivationRequestParameter
    @Property
    private Mode mode;

    @ActivationRequestParameter
    @Property
    private String courseUuid;

    // Screen fields

    @Property
    private Course course;

    // Work fields

    // This carries version through the redirect that follows a server-side
    // validation failure.
    @Persist(PersistenceConstants.FLASH)
    private Integer versionFlash;

    // Generally useful bits and pieces

    @Inject
    private CourseFinderService courseFinderService;

    @Inject
    private CourseManagerService courseManagerService;

    @Component
    private CustomForm createForm;

    @Inject
    private ComponentResources componentResources;

    @InjectPage
    private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage;

    // The code

    public void setup(Mode mode, String courseUuid) {
        this.mode = mode;
        this.courseUuid = courseUuid;
    }

    // setupRender() is called by Tapestry right before it starts rendering the
    // component.

    void setupRender() {

        if (mode == Mode.REVIEW) {
            if (courseUuid == null) {
                course = null;
                // Handle null course in the template.
            } else {
                if (course == null) {
                    try {
                        course = courseFinderService.findCourseByUuid(courseUuid);
                    } catch (ObjectNotFoundException e) {
                        // Handle null course in the template.
                    }
                }
            }
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // CREATE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "cancelCreate"

    Object onCancelCreate() {
        return indexPage;
    }

    // Component "createForm" bubbles up the PREPARE event when it is rendered
    // or submitted

    void onPrepareFromCreateForm() throws Exception {
        // Instantiate a Course for the form data to overlay.
        course = new Course();
    }

    // Component "createForm" bubbles up the VALIDATE event when it is submitted

    void onValidateFromCreateForm() {

        if (createForm.getHasErrors()) {
            // We get here only if a server-side validator detected an error.
            return;
        }

        try {
            course = courseManagerService.createCourse(course.getCode(), course.getName(), course.getSummary(),
                    course.getDescription(), 1);
        } catch (RestClientFailureException e) {
            createForm.recordError("Internal error on server.");
            createForm.recordError(e.getMessage());
        } catch (Exception e) {
            createForm.recordError(ExceptionUtil.getRootCauseMessage(e));
        }
    }

    // Component "createForm" bubbles up SUCCESS or FAILURE when it is
    // submitted, depending on whether VALIDATE
    // records an error

    boolean onSuccessFromCreateForm() {
        componentResources.triggerEvent(SUCCESSFUL_CREATE, new Object[] { course.getUuid() }, null);

        // We don't want "success" to bubble up, so we return true to say we've
        // handled it.
        mode = Mode.REVIEW;
        courseUuid = course.getUuid();
        return true;
    }

    boolean onFailureFromCreateForm() {
        // Rather than letting "failure" bubble up which doesn't say what you
        // were trying to do, we trigger new event
        // "failedCreate". It will bubble up because we don't have a handler
        // method for it.
        componentResources.triggerEvent(FAILED_CREATE, null, null);

        // We don't want "failure" to bubble up, so we return true to say we've
        // handled it.
        return true;
    }

    ....
}

REVIEW template

The “review” template is a simple table. It is wrapped in a form but that’s solely for the navigation buttons at the bottom of the page.

  1.     <t:if test="modeReview">
  2.         <h1>Review</h1>
  3.        
  4.         <strong>Warning: no attempt is made to block XSS</strong>
  5.  
  6.         <form t:type="form" t:id="reviewForm">
  7.             <t:errors/>
  8.        
  9.         <t:if test="course">
  10.             <div t:type="if" t:test="deleteMessage" class="error">
  11.                 ${deleteMessage}
  12.             </div>
  13.  
  14.             <table>
  15.                 <tr>
  16.                     <th>Uuid:</th>
  17.                     <td>${course.uuid}</td>
  18.                 </tr>
  19.                 <tr>
  20.                     <th>Code:</th>
  21.                     <td>${course.code}</td>
  22.                 </tr>
  23.                 <tr>
  24.                     <th>Name:</th>
  25.                     <td>${course.name}</td>
  26.                 </tr>
  27.                 <tr>
  28.                     <th>Summary:</th>
  29.                     <td>${course.summary}</td>
  30.                 </tr>
  31.                 <tr>
  32.                     <th>Description:</th>
  33.                     <td>${course.description}</td>
  34.                 </tr>
  35.             </table>
  36.  
  37.             <div class="buttons">
  38.                 <t:submit t:event="toIndex" t:context="course.uuid" value="List"/>
  39.                 <t:submit t:event="toUpdate" t:context="course.uuid" value="Update"/>
  40.                 <t:submit t:event="delete" t:context="course.uuid" t:mixins="Confirm" t:message="Delete ${course.name}?" value="Delete"/>
  41.             </div>
  42.  
  43.         </t:if>
  44.         <t:if negate="true" test="course">
  45.             Course ${courseUuid} does not exist.<br/><br/>
  46.         </t:if>
  47.         </form>
  48.     </t:if>
    <t:if test="modeReview">
        <h1>Review</h1>
        
        <strong>Warning: no attempt is made to block XSS</strong>

        <form t:type="form" t:id="reviewForm">
            <t:errors/>
        
        <t:if test="course">
            <div t:type="if" t:test="deleteMessage" class="error">
                ${deleteMessage}
            </div>

            <table>
                <tr>
                    <th>Uuid:</th>
                    <td>${course.uuid}</td>
                </tr>
                <tr>
                    <th>Code:</th>
                    <td>${course.code}</td>
                </tr>
                <tr>
                    <th>Name:</th>
                    <td>${course.name}</td>
                </tr>
                <tr>
                    <th>Summary:</th>
                    <td>${course.summary}</td>
                </tr>
                <tr>
                    <th>Description:</th>
                    <td>${course.description}</td>
                </tr>
            </table>

            <div class="buttons">
                <t:submit t:event="toIndex" t:context="course.uuid" value="List"/>
                <t:submit t:event="toUpdate" t:context="course.uuid" value="Update"/>
                <t:submit t:event="delete" t:context="course.uuid" t:mixins="Confirm" t:message="Delete ${course.name}?" value="Delete"/>
            </div>

        </t:if>
        <t:if negate="true" test="course">
            Course ${courseUuid} does not exist.<br/><br/>
        </t:if>
        </form>
    </t:if>

REVIEW java

The java required for the review form is trivial – we just need to load the data. I would have expected the setupRender() to be enough but in practice I needed the onPrepareFromReviewForm() method.

  1. public class Editor {
  2.  
  3.     public enum Mode {
  4.         CREATE, REVIEW, UPDATE;
  5.     }
  6.  
  7.     // Parameters
  8.  
  9.     @ActivationRequestParameter
  10.     @Property
  11.     private Mode mode;
  12.  
  13.     @ActivationRequestParameter
  14.     @Property
  15.     private String courseUuid;
  16.  
  17.     // Screen fields
  18.  
  19.     @Property
  20.     private Course course;
  21.  
  22.     // Generally useful bits and pieces
  23.  
  24.     @Inject
  25.     private CourseFinderService courseFinderService;
  26.  
  27.     @Component
  28.     private CustomForm reviewForm;
  29.  
  30.     @Inject
  31.     private ComponentResources componentResources;
  32.  
  33.     @InjectPage
  34.     private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage;
  35.  
  36.     // The code
  37.  
  38.     public void setup(Mode mode, String courseUuid) {
  39.         this.mode = mode;
  40.         this.courseUuid = courseUuid;
  41.     }
  42.  
  43.     // setupRender() is called by Tapestry right before it starts rendering the
  44.     // component.
  45.  
  46.     void setupRender() {
  47.  
  48.         if (mode == Mode.REVIEW) {
  49.             if (courseUuid == null) {
  50.                 course = null;
  51.                 // Handle null course in the template.
  52.             } else {
  53.                 if (course == null) {
  54.                     try {
  55.                         course = courseFinderService.findCourseByUuid(courseUuid);
  56.                     } catch (ObjectNotFoundException e) {
  57.                         // Handle null course in the template.
  58.                     }
  59.                 }
  60.             }
  61.         }
  62.     }
  63.  
  64.     // /////////////////////////////////////////////////////////////////////
  65.     // REVIEW
  66.     // /////////////////////////////////////////////////////////////////////
  67.  
  68.     void onPrepareFromReviewForm() {
  69.         try {
  70.             course = courseFinderService.findCourseByUuid(courseUuid);
  71.         } catch (ObjectNotFoundException e) {
  72.             // Handle null course in the template.
  73.         }
  74.     }
  75.  
  76.     // /////////////////////////////////////////////////////////////////////
  77.     // PAGE NAVIGATION
  78.     // /////////////////////////////////////////////////////////////////////
  79.  
  80.     // Handle event "toUpdate"
  81.  
  82.     boolean onToUpdate(String courseUuid) {
  83.         mode = Mode.UPDATE;
  84.         return false;
  85.     }
  86.  
  87.     // Handle event "toIndex"
  88.  
  89.     Object onToIndex() {
  90.         return indexPage;
  91.     }
  92.  
  93.     ....
  94. }
public class Editor {

    public enum Mode {
        CREATE, REVIEW, UPDATE;
    }

    // Parameters

    @ActivationRequestParameter
    @Property
    private Mode mode;

    @ActivationRequestParameter
    @Property
    private String courseUuid;

    // Screen fields

    @Property
    private Course course;

    // Generally useful bits and pieces

    @Inject
    private CourseFinderService courseFinderService;

    @Component
    private CustomForm reviewForm;

    @Inject
    private ComponentResources componentResources;

    @InjectPage
    private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage;

    // The code

    public void setup(Mode mode, String courseUuid) {
        this.mode = mode;
        this.courseUuid = courseUuid;
    }

    // setupRender() is called by Tapestry right before it starts rendering the
    // component.

    void setupRender() {

        if (mode == Mode.REVIEW) {
            if (courseUuid == null) {
                course = null;
                // Handle null course in the template.
            } else {
                if (course == null) {
                    try {
                        course = courseFinderService.findCourseByUuid(courseUuid);
                    } catch (ObjectNotFoundException e) {
                        // Handle null course in the template.
                    }
                }
            }
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // REVIEW
    // /////////////////////////////////////////////////////////////////////

    void onPrepareFromReviewForm() {
        try {
            course = courseFinderService.findCourseByUuid(courseUuid);
        } catch (ObjectNotFoundException e) {
            // Handle null course in the template.
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // PAGE NAVIGATION
    // /////////////////////////////////////////////////////////////////////

    // Handle event "toUpdate"

    boolean onToUpdate(String courseUuid) {
        mode = Mode.UPDATE;
        return false;
    }

    // Handle event "toIndex"

    Object onToIndex() {
        return indexPage;
    }

    ....
}

UPDATE template

Finally, the “update” template looks similar to the “create” template.

  1.     <t:if test="modeUpdate">
  2.         <h1>Update</h1>
  3.  
  4.         <strong>Warning: no attempt is made to block XSS</strong>
  5.        
  6.         <form t:type="form" t:id="updateForm">
  7.             <t:errors/>
  8.        
  9.             <t:if test="course">
  10.                 <!-- If optimistic locking is not needed then comment out this next line. It works because Hidden fields are part of the submit. -->
  11.                 <!-- <t:hidden value="course.version"/> -->
  12.        
  13.                 <table>
  14.                     <tr>
  15.                         <th><t:label for="updCode"/>:</th>
  16.                         <td><input t:type="TextField" t:id="updCode" value="course.code" t:disabled="true" size="12"/></td>
  17.                         <td>(read-only)</td>
  18.                     </tr>
  19.                     <tr class="err">
  20.                         <th></th>
  21.                         <td colspan="2"><t:CustomError for="updName"/></td>
  22.                     </tr>
  23.                     <tr>
  24.                         <th><t:label for="updName"/>:</th>
  25.                         <td><input t:type="TextField" t:id="updName" value="course.name" t:validate="required, maxlength=80" size="45"/></td>
  26.                         <td>(required)</td>
  27.                     </tr>
  28.                     <tr class="err">
  29.                         <th></th>
  30.                         <td colspan="2"><t:CustomError for="updSummary"/></td>
  31.                     </tr>
  32.                     <tr>
  33.                         <th><t:label for="updSummary"/>:</th>
  34.                         <td><input cols="50" rows="4" t:type="TextArea" t:id="updSummary" value="course.summary" t:validate="maxlength=400"/></td>
  35.                     </tr>
  36.                     <tr class="err">
  37.                         <th></th>
  38.                         <td colspan="2"><t:CustomError for="updSummary"/></td>
  39.                     </tr>
  40.                     <tr>
  41.                         <th><t:label for="updDescription"/>:</th>
  42.                         <td><input cols="50" rows="12" t:type="TextArea" t:id="updDescription" value="course.description" t:validate="maxlength=50"/></td>
  43.                     </tr>
  44.                     <tr class="err">
  45.                         <th></th>
  46.                         <td colspan="2"><t:CustomError for="updDescription"/></td>
  47.                     </tr>
  48.                 </table>
  49.  
  50.                 <div class="buttons">
  51.                     <t:submit t:event="toIndex" t:context="course.uuid" value="List"/>
  52.                     <t:submit t:event="cancelUpdate" t:context="course.uuid" value="Cancel"/>
  53.                     <input t:type="submit" value="Save"/>
  54.                 </div>
  55.             </t:if>
  56.             <t:if negate="true" test="course">
  57.                 Course ${courseUuid} does not exist.<br/><br/>
  58.             </t:if>
  59.         </form>    
  60.     </t:if>
    <t:if test="modeUpdate">
        <h1>Update</h1>

        <strong>Warning: no attempt is made to block XSS</strong>
        
        <form t:type="form" t:id="updateForm">
            <t:errors/>
        
            <t:if test="course">
                <!-- If optimistic locking is not needed then comment out this next line. It works because Hidden fields are part of the submit. -->
                <!-- <t:hidden value="course.version"/> -->
        
                <table>
                    <tr>
                        <th><t:label for="updCode"/>:</th>
                        <td><input t:type="TextField" t:id="updCode" value="course.code" t:disabled="true" size="12"/></td>
                        <td>(read-only)</td>
                    </tr>
                    <tr class="err">
                        <th></th>
                        <td colspan="2"><t:CustomError for="updName"/></td>
                    </tr>
                    <tr>
                        <th><t:label for="updName"/>:</th>
                        <td><input t:type="TextField" t:id="updName" value="course.name" t:validate="required, maxlength=80" size="45"/></td>
                        <td>(required)</td>
                    </tr>
                    <tr class="err">
                        <th></th>
                        <td colspan="2"><t:CustomError for="updSummary"/></td>
                    </tr>
                    <tr>
                        <th><t:label for="updSummary"/>:</th>
                        <td><input cols="50" rows="4" t:type="TextArea" t:id="updSummary" value="course.summary" t:validate="maxlength=400"/></td>
                    </tr>
                    <tr class="err">
                        <th></th>
                        <td colspan="2"><t:CustomError for="updSummary"/></td>
                    </tr>
                    <tr>
                        <th><t:label for="updDescription"/>:</th>
                        <td><input cols="50" rows="12" t:type="TextArea" t:id="updDescription" value="course.description" t:validate="maxlength=50"/></td>
                    </tr>
                    <tr class="err">
                        <th></th>
                        <td colspan="2"><t:CustomError for="updDescription"/></td>
                    </tr>
                </table>

                <div class="buttons">
                    <t:submit t:event="toIndex" t:context="course.uuid" value="List"/>
                    <t:submit t:event="cancelUpdate" t:context="course.uuid" value="Cancel"/>
                    <input t:type="submit" value="Save"/>
                </div>
            </t:if>
            <t:if negate="true" test="course">
                Course ${courseUuid} does not exist.<br/><br/>
            </t:if>
        </form>    
    </t:if>

UPDATE java

Likewise the “update” java code looks a lot like the “create” java code. The biggest difference is that we have to be able to handle a race condition where a course has been deleted before we attempt to update the database.

  1. @Events({ Editor.TO_UPDATE, Editor.CANCEL_UPDATE,
  2.         Editor.SUCCESSFUL_UPDATE, Editor.FAILED_UPDATE })
  3. public class Editor {
  4.     public static final String TO_UPDATE = "toUpdate";
  5.     public static final String CANCEL_UPDATE = "cancelUpdate";
  6.     public static final String SUCCESSFUL_UPDATE = "successfulUpdate";
  7.     public static final String FAILED_UPDATE = "failedUpdate";
  8.  
  9.     public enum Mode {
  10.         CREATE, REVIEW, UPDATE;
  11.     }
  12.  
  13.     // Parameters
  14.  
  15.     @ActivationRequestParameter
  16.     @Property
  17.     private Mode mode;
  18.  
  19.     @ActivationRequestParameter
  20.     @Property
  21.     private String courseUuid;
  22.  
  23.     // Screen fields
  24.  
  25.     @Property
  26.     private Course course;
  27.  
  28.     @Property
  29.     @Persist(PersistenceConstants.FLASH)
  30.     private String deleteMessage;
  31.  
  32.     // Work fields
  33.  
  34.     // This carries version through the redirect that follows a server-side
  35.     // validation failure.
  36.     @Persist(PersistenceConstants.FLASH)
  37.     private Integer versionFlash;
  38.  
  39.     // Generally useful bits and pieces
  40.  
  41.     @Inject
  42.     private CourseFinderService courseFinderService;
  43.  
  44.     @Inject
  45.     private CourseManagerService courseManagerService;
  46.  
  47.     @Component
  48.     private CustomForm updateForm;
  49.  
  50.     @Inject
  51.     private ComponentResources componentResources;
  52.  
  53.     @InjectPage
  54.     private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage;
  55.  
  56.     // The code
  57.  
  58.     public void setup(Mode mode, String courseUuid) {
  59.         this.mode = mode;
  60.         this.courseUuid = courseUuid;
  61.     }
  62.  
  63.     // setupRender() is called by Tapestry right before it starts rendering the
  64.     // component.
  65.  
  66.     void setupRender() {
  67.  
  68.         if (mode == Mode.REVIEW) {
  69.             if (courseUuid == null) {
  70.                 course = null;
  71.                 // Handle null course in the template.
  72.             } else {
  73.                 if (course == null) {
  74.                     try {
  75.                         course = courseFinderService.findCourseByUuid(courseUuid);
  76.                     } catch (ObjectNotFoundException e) {
  77.                         // Handle null course in the template.
  78.                     }
  79.                 }
  80.             }
  81.         }
  82.     }
  83.  
  84.     // /////////////////////////////////////////////////////////////////////
  85.     // UPDATE
  86.     // /////////////////////////////////////////////////////////////////////
  87.  
  88.     // Handle event "cancelUpdate"
  89.  
  90.     Object onCancelUpdate(String courseUuid) {
  91.         return indexPage;
  92.     }
  93.  
  94.     // Component "updateForm" bubbles up the PREPARE_FOR_RENDER event during
  95.     // form render
  96.  
  97.     void onPrepareForRenderFromUpdateForm() {
  98.         try {
  99.             course = courseFinderService.findCourseByUuid(courseUuid);
  100.         } catch (ObjectNotFoundException e) {
  101.             // Handle null course in the template.
  102.         }
  103.  
  104.         // If the form has errors then we're redisplaying after a redirect.
  105.         // Form will restore your input values but it's up to us to restore
  106.         // Hidden values.
  107.  
  108.         if (updateForm.getHasErrors()) {
  109.             if (course != null) {
  110.                 course.setVersion(versionFlash);
  111.             }
  112.         }
  113.     }
  114.  
  115.     // Component "updateForm" bubbles up the PREPARE_FOR_SUBMIT event during for
  116.     // submission
  117.  
  118.     void onPrepareForSubmitFromUpdateForm() {
  119.         // Get objects for the form fields to overlay.
  120.         try {
  121.             course = courseFinderService.findCourseByUuid(courseUuid);
  122.         } catch (ObjectNotFoundException e) {
  123.             course = new Course();
  124.             updateForm.recordError("Course has been deleted by another process.");
  125.         }
  126.     }
  127.  
  128.     // Component "updateForm" bubbles up the VALIDATE event when it is submitted
  129.  
  130.     void onValidateFromUpdateForm() {
  131.  
  132.         if (updateForm.getHasErrors()) {
  133.             // We get here only if a server-side validator detected an error.
  134.             return;
  135.         }
  136.  
  137.         try {
  138.             courseManagerService
  139.                     .updateCourse(course, course.getName(), course.getSummary(), course.getDescription(), 1);
  140.         } catch (RestClientFailureException e) {
  141.             updateForm.recordError("Internal error on server.");
  142.             updateForm.recordError(e.getMessage());
  143.         } catch (Exception e) {
  144.             // Display the cause. In a real system we would try harder to get a
  145.             // user-friendly message.
  146.             updateForm.recordError(ExceptionUtil.getRootCauseMessage(e));
  147.         }
  148.     }
  149.  
  150.     // Component "updateForm" bubbles up SUCCESS or FAILURE when it is
  151.     // submitted, depending on whether VALIDATE
  152.     // records an error
  153.  
  154.     boolean onSuccessFromUpdateForm() {
  155.         // We want to tell our containing page explicitly what course we've
  156.         // updated, so we trigger new event
  157.         // "successfulUpdate" with a parameter. It will bubble up because we
  158.         // don't have a handler method for it.
  159.         componentResources.triggerEvent(SUCCESSFUL_UPDATE, new Object[] { courseUuid }, null);
  160.  
  161.         // We don't want "success" to bubble up, so we return true to say we've
  162.         // handled it.
  163.         mode = Mode.REVIEW;
  164.         return true;
  165.     }
  166.  
  167.     boolean onFailureFromUpdateForm() {
  168.         versionFlash = course.getVersion();
  169.  
  170.         // Rather than letting "failure" bubble up which doesn't say what you
  171.         // were trying to do, we trigger new event
  172.         // "failedUpdate". It will bubble up because we don't have a handler
  173.         // method for it.
  174.         componentResources.triggerEvent(FAILED_UPDATE, new Object[] { courseUuid }, null);
  175.         // We don't want "failure" to bubble up, so we return true to say we've
  176.         // handled it.
  177.         return true;
  178.     }
  179. }
@Events({ Editor.TO_UPDATE, Editor.CANCEL_UPDATE,
        Editor.SUCCESSFUL_UPDATE, Editor.FAILED_UPDATE })
public class Editor {
    public static final String TO_UPDATE = "toUpdate";
    public static final String CANCEL_UPDATE = "cancelUpdate";
    public static final String SUCCESSFUL_UPDATE = "successfulUpdate";
    public static final String FAILED_UPDATE = "failedUpdate";
 
    public enum Mode {
        CREATE, REVIEW, UPDATE;
    }

    // Parameters

    @ActivationRequestParameter
    @Property
    private Mode mode;

    @ActivationRequestParameter
    @Property
    private String courseUuid;

    // Screen fields

    @Property
    private Course course;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private String deleteMessage;

    // Work fields

    // This carries version through the redirect that follows a server-side
    // validation failure.
    @Persist(PersistenceConstants.FLASH)
    private Integer versionFlash;

    // Generally useful bits and pieces

    @Inject
    private CourseFinderService courseFinderService;

    @Inject
    private CourseManagerService courseManagerService;

    @Component
    private CustomForm updateForm;

    @Inject
    private ComponentResources componentResources;

    @InjectPage
    private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage;

    // The code

    public void setup(Mode mode, String courseUuid) {
        this.mode = mode;
        this.courseUuid = courseUuid;
    }

    // setupRender() is called by Tapestry right before it starts rendering the
    // component.

    void setupRender() {

        if (mode == Mode.REVIEW) {
            if (courseUuid == null) {
                course = null;
                // Handle null course in the template.
            } else {
                if (course == null) {
                    try {
                        course = courseFinderService.findCourseByUuid(courseUuid);
                    } catch (ObjectNotFoundException e) {
                        // Handle null course in the template.
                    }
                }
            }
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // UPDATE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "cancelUpdate"

    Object onCancelUpdate(String courseUuid) {
        return indexPage;
    }

    // Component "updateForm" bubbles up the PREPARE_FOR_RENDER event during
    // form render

    void onPrepareForRenderFromUpdateForm() {
        try {
            course = courseFinderService.findCourseByUuid(courseUuid);
        } catch (ObjectNotFoundException e) {
            // Handle null course in the template.
        }

        // If the form has errors then we're redisplaying after a redirect.
        // Form will restore your input values but it's up to us to restore
        // Hidden values.

        if (updateForm.getHasErrors()) {
            if (course != null) {
                course.setVersion(versionFlash);
            }
        }
    }

    // Component "updateForm" bubbles up the PREPARE_FOR_SUBMIT event during for
    // submission

    void onPrepareForSubmitFromUpdateForm() {
        // Get objects for the form fields to overlay.
        try {
            course = courseFinderService.findCourseByUuid(courseUuid);
        } catch (ObjectNotFoundException e) {
            course = new Course();
            updateForm.recordError("Course has been deleted by another process.");
        }
    }

    // Component "updateForm" bubbles up the VALIDATE event when it is submitted

    void onValidateFromUpdateForm() {

        if (updateForm.getHasErrors()) {
            // We get here only if a server-side validator detected an error.
            return;
        }

        try {
            courseManagerService
                    .updateCourse(course, course.getName(), course.getSummary(), course.getDescription(), 1);
        } catch (RestClientFailureException e) {
            updateForm.recordError("Internal error on server.");
            updateForm.recordError(e.getMessage());
        } catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a
            // user-friendly message.
            updateForm.recordError(ExceptionUtil.getRootCauseMessage(e));
        }
    }

    // Component "updateForm" bubbles up SUCCESS or FAILURE when it is
    // submitted, depending on whether VALIDATE
    // records an error

    boolean onSuccessFromUpdateForm() {
        // We want to tell our containing page explicitly what course we've
        // updated, so we trigger new event
        // "successfulUpdate" with a parameter. It will bubble up because we
        // don't have a handler method for it.
        componentResources.triggerEvent(SUCCESSFUL_UPDATE, new Object[] { courseUuid }, null);

        // We don't want "success" to bubble up, so we return true to say we've
        // handled it.
        mode = Mode.REVIEW;
        return true;
    }

    boolean onFailureFromUpdateForm() {
        versionFlash = course.getVersion();

        // Rather than letting "failure" bubble up which doesn't say what you
        // were trying to do, we trigger new event
        // "failedUpdate". It will bubble up because we don't have a handler
        // method for it.
        componentResources.triggerEvent(FAILED_UPDATE, new Object[] { courseUuid }, null);
        // We don't want "failure" to bubble up, so we return true to say we've
        // handled it.
        return true;
    }
}

DELETE template and java

The editor doesn’t have an explicit “delete” mode but it does support deleting the current object on the review and update pages.

  1.     // /////////////////////////////////////////////////////////////////////
  2.     // DELETE
  3.     // /////////////////////////////////////////////////////////////////////
  4.  
  5.     // Handle event "delete"
  6.  
  7.     Object onDelete(String courseUuid) {
  8.         this.courseUuid = courseUuid;
  9.         int courseVersion = 0;
  10.  
  11.         try {
  12.             courseManagerService.deleteCourse(courseUuid, courseVersion);
  13.         } catch (ObjectNotFoundException e) {
  14.             // the object's already deleted
  15.         } catch (RestClientFailureException e) {
  16.             createForm.recordError("Internal error on server.");
  17.             createForm.recordError(e.getMessage());
  18.  
  19.             // Display the cause. In a real system we would try harder to get a
  20.             // user-friendly message.
  21.             deleteMessage = ExceptionUtil.getRootCauseMessage(e);
  22.  
  23.             // Trigger new event "failedDelete" which will bubble up.
  24.             componentResources.triggerEvent(FAILED_DELETE, new Object[] { courseUuid }, null);
  25.             // We don't want "delete" to bubble up, so we return true to say
  26.             // we've handled it.
  27.             return true;
  28.         } catch (Exception e) {
  29.             // Display the cause. In a real system we would try harder to get a
  30.             // user-friendly message.
  31.             deleteMessage = ExceptionUtil.getRootCauseMessage(e);
  32.  
  33.             // Trigger new event "failedDelete" which will bubble up.
  34.             componentResources.triggerEvent(FAILED_DELETE, new Object[] { courseUuid }, null);
  35.             // We don't want "delete" to bubble up, so we return true to say
  36.             // we've handled it.
  37.             return true;
  38.         }
  39.  
  40.         // Trigger new event "successfulDelete" which will bubble up.
  41.         componentResources.triggerEvent(SUCCESFUL_DELETE, new Object[] { courseUuid }, null);
  42.         // We don't want "delete" to bubble up, so we return true to say we've
  43.         // handled it.
  44.         return indexPage;
  45.     }
    // /////////////////////////////////////////////////////////////////////
    // DELETE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "delete"

    Object onDelete(String courseUuid) {
        this.courseUuid = courseUuid;
        int courseVersion = 0;

        try {
            courseManagerService.deleteCourse(courseUuid, courseVersion);
        } catch (ObjectNotFoundException e) {
            // the object's already deleted
        } catch (RestClientFailureException e) {
            createForm.recordError("Internal error on server.");
            createForm.recordError(e.getMessage());

            // Display the cause. In a real system we would try harder to get a
            // user-friendly message.
            deleteMessage = ExceptionUtil.getRootCauseMessage(e);

            // Trigger new event "failedDelete" which will bubble up.
            componentResources.triggerEvent(FAILED_DELETE, new Object[] { courseUuid }, null);
            // We don't want "delete" to bubble up, so we return true to say
            // we've handled it.
            return true;
        } catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a
            // user-friendly message.
            deleteMessage = ExceptionUtil.getRootCauseMessage(e);

            // Trigger new event "failedDelete" which will bubble up.
            componentResources.triggerEvent(FAILED_DELETE, new Object[] { courseUuid }, null);
            // We don't want "delete" to bubble up, so we return true to say
            // we've handled it.
            return true;
        }

        // Trigger new event "successfulDelete" which will bubble up.
        componentResources.triggerEvent(SUCCESFUL_DELETE, new Object[] { courseUuid }, null);
        // We don't want "delete" to bubble up, so we return true to say we've
        // handled it.
        return indexPage;
    }

Next Steps

The obvious next steps are improving the error messages, adding support for pagination, support, and one-to-many and many-to-many relationships. All will require revising the REST payloads. I have a few additional items in the pipeline, e.g., an ExceptionService, to say nothing of the security issues.

Source Code

The source code is at https://github.com/beargiles/project-student [github] and http://beargiles.github.io/project-student/ [github pages].

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

Archives

  • May 2020 (1)
  • March 2019 (1)
  • August 2018 (1)
  • May 2018 (1)
  • February 2018 (1)
  • November 2017 (4)
  • January 2017 (3)
  • June 2016 (1)
  • May 2016 (1)
  • April 2016 (2)
  • March 2016 (1)
  • February 2016 (3)
  • January 2016 (6)
  • December 2015 (2)
  • November 2015 (3)
  • October 2015 (2)
  • August 2015 (4)
  • July 2015 (2)
  • June 2015 (2)
  • January 2015 (1)
  • December 2014 (6)
  • October 2014 (1)
  • September 2014 (2)
  • August 2014 (1)
  • July 2014 (1)
  • June 2014 (2)
  • May 2014 (2)
  • April 2014 (1)
  • March 2014 (1)
  • February 2014 (3)
  • January 2014 (6)
  • December 2013 (13)
  • November 2013 (6)
  • October 2013 (3)
  • September 2013 (2)
  • August 2013 (5)
  • June 2013 (1)
  • May 2013 (2)
  • March 2013 (1)
  • November 2012 (1)
  • October 2012 (3)
  • September 2012 (2)
  • May 2012 (6)
  • January 2012 (2)
  • December 2011 (12)
  • July 2011 (1)
  • June 2011 (2)
  • May 2011 (5)
  • April 2011 (6)
  • March 2011 (4)
  • February 2011 (3)
  • October 2010 (6)
  • September 2010 (8)

Recent Posts

  • 8-bit Breadboard Computer: Good Encapsulation!
  • Where are all the posts?
  • Better Ad Blocking Through Pi-Hole and Local Caching
  • The difference between APIs and SPIs
  • Hadoop: User Impersonation with Kerberos Authentication

Meta

  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org

Pages

  • About Me
  • Notebook: Common XML Tasks
  • Notebook: Database/Webapp Security
  • Notebook: Development Tips

Syndication

Java Code Geeks

Know Your Rights

Support Bloggers' Rights
Demand Your dotRIGHTS

Security

  • Dark Reading
  • Krebs On Security Krebs On Security
  • Naked Security Naked Security
  • Schneier on Security Schneier on Security
  • TaoSecurity TaoSecurity

Politics

  • ACLU ACLU
  • EFF EFF

News

  • Ars technica Ars technica
  • Kevin Drum at Mother Jones Kevin Drum at Mother Jones
  • Raw Story Raw Story
  • Tech Dirt Tech Dirt
  • Vice Vice

Spam Blocked

53,314 spam blocked by Akismet
rss Comments rss valid xhtml 1.1 design by jide powered by Wordpress get firefox