First Topaz Application

This example is loosely based on the work done at http://www.w3.org/2005/Incubator/mmsem/XGR-image-annotation/, specifically Section 5.1, Use Case: Management of Personal Digital Photo Collections.

Let's assume we need a small application that can store photos as well as some simple meta data along with it. We will use an embedded Mulgara database and an embedded [<insert-link> SimpleBlobStore?] for these examples so that the configuration is minimal.

For the impatient, see the section Running it to see how to download the complete source and run. This requires that you have maven installed on your system.

Creating a Maven Project

The first thing we need to do is set up our development environment, and specifically to setup all the required dependencies to Topaz as well as other libraries. Topaz is built using Maven which amongst other features provides dependency management; moreover it provides transitive dependency management which simply means that to use Topaz we can simply define our dependency on Topaz, Topaz itself defines the dependencies it needs which then become transitive dependencies of our project.

Maven can be downloaded and installed from http://maven.apache.org. The first step in following along this introduction is to ensure that you have a working maven install.

Next step is to create a new Maven project. Please see the maven archeType plugin documentation to see how a new project can be started in Maven. For this example we are creating a new project by running 'mvn archetype:generate' and choosing the following options when prompted:

  • type of project : maven-archetype-webapp
  • groupId : org.topazproject
  • artifactId : topaz-photo-example
  • package : org.topazproject.examples.photo

This should create a project directory and some starter files. Note that the 'package' directory may not be created for you. If this happens manually create src/main/java/org/topazproject/examples/photo for the location of our java source files.

The resulting project directory tree should now look something like this:

tree
.
|-- pom.xml
`-- src
    `-- main
        |-- java
        |   `-- org
        |       `-- topazproject
        |           `-- examples
        |               `-- photo
        |-- resources
        `-- webapp
            `-- WEB-INF
                `-- web.xml

Important: The maven-archetype-webapp may create an index.jsp file. If it does, please delete this file. The jetty plugin that we use in this tutorial fails to compile jsp files.

Editing pom.xml

The pom.xml file located at the project root can now be edited to add topaz dependencies as shown below:

<?xml version="1.0" encoding="UTF-8"?>
<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/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>org.topazproject</groupId>
  <artifactId>topaz-photo-example</artifactId>
  <version>0.1-SNAPSHOT</version>
  <packaging>war</packaging>
  <name>Topaz Photo Example</name>
  <description>Simple photo management using Topaz.</description>

  <!-- 
       Topaz and Mulgara are not currently available on standard maven
       repositories. However they are hosted on Topaz maven repository.
       The following section lets maven know where that is.
   -->
  <repositories>
    <repository>
      <id>topaz</id>
      <name>Maven 2 Repository for Topaz</name>
      <url>http://maven.topazproject.org/maven2/</url>
    </repository>
  </repositories>

  <!--
      Maven defaults to java 1.3 on its compiler config.
      Our examples all need Java 1.5 annotations. So
      let Maven know about our requirements.
   -->
  <properties>
    <maven.compiler.source>1.5</maven.compiler.source>
    <maven.compiler.target>1.5</maven.compiler.target>
  </properties>

  <dependencies>
    <!-- The Topaz core jar file. -->
    <dependency>
      <groupId>org.topazproject</groupId>
      <artifactId>otm</artifactId>
      <version>0.9.1-SNAPSHOT</version>
    </dependency>
    <!-- The Mulgara client driver. -->
    <dependency>
      <groupId>org.mulgara</groupId>
      <artifactId>driver</artifactId>
      <version>2.0.7</version>
    </dependency>
    <!-- The Embedded Mulgara triple-store. 
         Only required during run time. -->
    <dependency>
      <groupId>org.mulgara</groupId>
      <artifactId>mulgara</artifactId>
      <version>2.0.7</version>
      <scope>runtime</scope>
    </dependency>
    <!-- A Mulgara Resolver that provides support
         for string comparisons (lt, gt, le, ge).
         Only required during run time. -->   
    <dependency>
      <groupId>org.topazproject</groupId>
      <artifactId>string-compare-resolver</artifactId>
      <version>0.9.1-SNAPSHOT</version>
      <scope>runtime</scope>
    </dependency>
    <!-- Servlet api is required to compile the servlets -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.4</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
  <build>
    <finalName>topaz-photo-example</finalName>
    <plugins>
      <!-- Run the web-app in jetty: 'mvn clean jetty:run' -->
      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>maven-jetty-plugin</artifactId>
        <version>6.1.14</version>
      </plugin>
    </plugins>
  </build>
</project>

Try a 'mvn clean install' here and it should build a war file with contents that looks something similar to the following:

(Note the actual versions of the jar files may be different)

tree target/topaz-photo-example
target/topaz-photo-example
|-- META-INF
`-- WEB-INF
    |-- classes
    |-- lib
    |   |-- activation-1.1.jar
    |   |-- ant-1.7.0.jar
    |   |-- ant-launcher-1.7.0.jar
    |   |-- antlr-2.7.7.jar
    |   |-- btm-1.3.jar
    |   |-- commons-io-1.4.jar
    |   |-- commons-lang-2.4.jar
    |   |-- commons-logging-1.1.1.jar
    |   |-- driver-2.0.7.jar
    |   |-- groovy-all-1.5.7.jar
    |   |-- javassist-3.4.ga.jar
    |   |-- jline-0.9.94.jar
    |   |-- jta-1.0.1B.jar
    |   |-- junit-3.8.2.jar
    |   |-- log4j-1.2.14.jar
    |   |-- mulgara-2.0.7.jar
    |   |-- mulgara-client-0.9.1.jar
    |   |-- otm-0.9.1.jar
    |   |-- slf4j-api-1.5.2.jar
    |   |-- slf4j-log4j12-1.5.2.jar
    |   `-- string-compare-resolver-0.9.1.jar
    `-- web.xml

Doing a test-run

Notice that the pom.xml contains a jetty-plugin. See http://mojo.codehaus.org/jetty-maven-plugin/usage.html for additional configuration information. The jetty-plugin allows starting up jetty and running the web-application in it. The command 'mvn jetty:run-war' should start it up. By default the listener is on port 8080. You can point your browser now to http://localhost:8080/topaz-photo-example to ensure that things are all working fine.

Important note for running with tomcat

Note that the embedded mulgara that we use (org.mulgara:mulgara in pom.xml) will not work with some versions of tomcat (notably tomcat 5.5). If this happens, switch to org.mulgara:mulgara-raw.

You may also have to ensure log4j jar file is not packaged in the war file. This requires identifying the dependency that is bringing in log4j and excluding it in the pom.xml. The command 'mvn dependency:tree' helps in identifying the dependency.

Creating Persistent Classes

Now that the environment is all set up and working it is time to create our first class.

package org.topazproject.examples.photo;

import java.net.URI;
import java.util.Date;

import org.topazproject.otm.annotations.Entity;
import org.topazproject.otm.annotations.Id;
import org.topazproject.otm.annotations.Predicate;

@Entity(graph="photo", types={"topaz:Photo"})
public class Photo {
  private URI id;
  private String title;
  private Date date;

  public URI getId() {return id;}
  @Id
  public void setId(URI id) {this.id = id;}

  public String getTitle() {return title;}
  @Predicate(uri="dc:title")
  public void setTitle(String title) {this.title = title;}

  public Date getDate(){return date;}
  @Predicate(uri="dc:date")
  public void setDate(Date date) {this.date = date;}
}

Notice that this is a simple POJO with 2 properties, a 'title' and 'date' that both refer to a Photo with a given Id. As you can see this class uses standard JavaBean? naming conventions for property getter and setter methods, as well as private visibility for the fields. This is a requirement. The no-argument constructor is also required to instantiate an object of this class through reflection.

The id property holds a unique identifier URI for a particular photo. All persistent entity classes will need such an identifier property and it represents the resource (subject-node) in RDF. The rest of the properties then represent the predicate-object pairs for the subject-node and represents the statements about this resource.

We usually don't manipulate the identity of an object, hence the application should not change the identifier of an object once it is attached to a Topaz persistent Session. (More on that later)

Place this file in the src/main/java/org/topazproject/examples/photo directory.

Mapping explained

Notice the java annotations on the class and the three setter methods in Photo.java. These annotations are the instructions to Topaz on how to map this class instances to RDF.

  • @Entity(graph="photo", types={"topaz:Photo"}) identifies this java class as something that Topaz should persist with the following additional configuration options:
    • graph="photo" specifies the named graph within the triple-store where statements about instances of Photo.class are to be stored.
    • types={"topaz:Photo"} identifies the rdf:type value that is unique to Photo.class instances.
  • @Id denotes the field that holds the identifier property (ie. the subject-uri of the RDF resource.)
  • @Predicate(uri="dc:title") and @Predicate(uri="dc:date) denotes the fields that contain property values for the corresponding predicate-uris. Note that there are lot more options on the @Predicate annotation. But for this simple example the defaults are sufficient. The defaults in this case would mean dc:title values are untyped literals and dc:date values are typed literals of type xsd:dateTime.

The package-info.java file

The mapping annotations above themselves are not sufficient for Topaz to persist instances of the Photo.class. Additional information is needed in order for topaz to translate from the graph name 'photo' to a named-graph URI that the triple-stores can work with. Similarly the aliases for the predicate URIs dc:title and dc:date as well as the rdf:type topaz:Photo needs to be configured.

Topaz provides package level annotations to configure the graphs and aliases. These annotations therefore can certainly be included in the Photo.java source file. However because of its global nature, the package-info.java file is better suited for this.

@Graphs({
  @Graph(id = "str", uri = "local:///topazproject#str",
         type = "http://topazproject.org/graphs#StringCompare"),
  @Graph(id = "xsd", uri = "local:///topazproject#xsd",
         type = Rdf.mulgara + "XMLSchemaModel"),
  @Graph(id = "prefix", uri = "local:///topazproject#prefix",
         type = Rdf.mulgara + "PrefixGraph"),
  @Graph(id = "photo", uri = "local:///topazproject#photo")
})

@Aliases({
  @Alias(alias = "dc",       value = Rdf.dc),
  @Alias(alias = "topaz",    value = Rdf.topaz)
})

package org.topazproject.examples.photo;

import org.topazproject.otm.annotations.Alias;
import org.topazproject.otm.annotations.Aliases;
import org.topazproject.otm.annotations.Graph;
import org.topazproject.otm.annotations.Graphs;
import org.topazproject.otm.Rdf;

Notice also that we have taken this opportunity to define three other graphs here. It is sort of a boiler-plate definition that comes in handy in most applications. The graph aliases that we use corresponds to the default used by Topaz. It is possible to change these defaults and becomes clearer in later chapters. For now a quick overview is as follows:

  • 'str' - this graph corresponds to the StringCompare? resolver in Mulgara. It is required to do string-compare (lt, gt, le, ge) operations.
  • 'xsd' - this corresponds to the xsd graph in Mulgara. Useful when querying literal data-types. Currently an optional for Topaz. But applications are encouraged to define this.
  • 'prefix' - this corresponds to the prefix-resolver in Mulgara. Needed mainly to support rdf:bag collections.

Place this file also in the src/main/java/org/topazproject/examples/photo directory.

The topaz.xml file

Now that we have a java class and the mapping configurations all created, the next step is in making sure Topaz is aware of our class. There are couple of ways to do this. One is to directly registering the Photo.class with Topaz and the other is letting Topaz scan the class-path for all annotated classes. The second option requires the presence of a marker file (topaz.xml) in the class-path and that is the option we will use for this example.

<?xml version="1.0" encoding="UTF-8"?>
<topaz/>

Place this file in the src/main/resources directory.

Configuring Topaz

It is now time to configure a Triple-Store to store and load instances of the Photo.class entity. Embedded Mulgara is the easiest to configure. The following is a configuration class that isolates the Topaz configurations from rest of the application. This class builds a Topaz SessionFactory object and configures it. The configuration involves the following:

  • configuring an embedded Mulgara with the database located in the tmp directory. Edit this and change the location as required.
  • load all annotated classes. The call to preloadFromClassPath() will cause Topaz to scan the class-path components and isolate the components that contain a topaz.xml file. It then scans the classes in those components for the presence of Topaz annotations like @Entity and registers them. This will now cause the Photo.class to be registered. Also note that the annotations in package-info.java is also picked up when processing the Photo.class. So Topaz is now aware of all of our mappings and configurations.
  • validate the configurations. The call to validate() does this. This step ensures that all the prior configurations are internally consistent for Topaz to work with.
  • create all configured named graphs in Mulgara. See the createGraphs() method. The code here is representative of the pattern in which applications interact with Topaz and you will see it repeated a few times in this document. It will become clearer with explanations in later chapters. For now understand that all the graphs that were configured using @Graph annotation is created in Mulgara. Graph creations are idempotent in Mulgara. So there is no need for us to ensure the non-existence of graphs before performing the graph creation operation.
package org.topazproject.examples.photo;

import java.net.URI;
import org.topazproject.mulgara.itql.DefaultItqlClientFactory;

import org.topazproject.otm.GraphConfig;
import org.topazproject.otm.OtmException;
import org.topazproject.otm.Session;
import org.topazproject.otm.SessionFactory;
import org.topazproject.otm.impl.SessionFactoryImpl;
import org.topazproject.otm.stores.ItqlStore;

public class TopazConfigurator {
  private SessionFactory factory = new SessionFactoryImpl();

  public TopazConfigurator() throws OtmException {
    DefaultItqlClientFactory tqlFactory = new DefaultItqlClientFactory();
    tqlFactory.setDbDir(System.getProperty("java.io.tmpdir") + "/triple-db");
    ItqlStore tripleStore = new ItqlStore(URI.create("local:///topazproject"), tqlFactory);
    factory.setTripleStore(tripleStore);
    factory.preloadFromClasspath();
    factory.validate();
    createGraphs();
  }

  public SessionFactory getSessionFactory() {
    return factory;
  }

  private void createGraphs() throws OtmException {
    Session session = factory.openSession();
    try {
      session.beginTransaction();
      for (GraphConfig gc : factory.listGraphs())
        session.createGraph(gc.getId());
      session.getTransaction().commit();
    } finally {
      try { session.close(); } catch (Throwable t) {}
    }
  }
}

Place this class also in the src/main/java/org/topazproject/examples/photo directory.

The log4j configuration

Topaz uses commons-logging for logging. Notice that because of transitive dependency from including embedded Mulgara, the log4j jar file is already included in our project. So we'll go with log4j as the logger to use for this example and place a log4j.xml file in the src/main/resources directory.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">

<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
  <appender name="console" class="org.apache.log4j.ConsoleAppender">
    <param name="Target" value="System.out"/>
    <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern" value="%d{ISO8601} %-5p %c{1}&gt; %m%n"/>
    </layout>
  </appender>

  <logger name="org.topazproject.otm">
    <level value="info" />
  </logger>

  <root>
    <priority value ="info" />
    <appender-ref ref="console" />
  </root>

</log4j:configuration>

Note that if you would like to configure log4j at the container level, the log4j jar can be excluded in the pom.xml. See the tree Maven Dependencies documentation for help in excluding this.

Also note that Topaz does not directly have a dependency on log4j. So if a future version of Mulgara excludes the log4j dependency, you may need to add an explicit dependency in the pom.xml.

With this the infrastructure for this project is complete. The directory tree should look like this now:

tree
.
|-- pom.xml
`-- src
    `-- main
        |-- java
        |   `-- org
        |       `-- topazproject
        |           `-- examples
        |               `-- photo
        |                   |-- Photo.java
        |                   |-- TopazConfigurator.java
        |                   `-- package-info.java
        |-- resources
        |   |-- log4j.xml
        |   `-- topaz.xml
        `-- webapp
            |-- WEB-INF
                `-- web.xml

Loading and Storing Objects

In Topaz all work happens in work Sessions. Sessions are often short lived and are created by the application to work with a specific task.

Topaz does not provide any synchronization on the operations over a Session. If concurrent access is required, the application must provide external synchronization. However because of lazy on-demand loading performed by Topaz, these synchronizations are hard to implement by an application. Therefore in a multi-threaded application like this web-application example we are working with, it is better to create a new Sessions to handle a request and close and discard when we are done processing the request.

In a more complex web application, it is certainly possible to create more than one Session during the course of servicing a request. However sharing a single Session will be more advantageous since that will better make use of the object cache that the Session provides.

In addition to Sessions, a Transaction needs to be started to work with the stores. See the [<insert-link> Transactions] chapter for working with Transactions.

In this example a Session is created and a transaction started when we receive a request and the transaction committed and the Session closed when we are done processing the request. In addition a Service class that isolates manipulating the POJOs is created as shown below:

package org.topazproject.examples.photo;

import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.topazproject.otm.OtmException;
import org.topazproject.otm.Session;
import org.topazproject.otm.SessionFactory;
import org.topazproject.otm.query.Results;

public class PhotoService {

  public void createPhoto(Session session, URI id, String title) throws OtmException {
    Photo photo = session.get(Photo.class, id.toString());
    if (photo == null) {
      photo = new Photo();
      photo.setId(id);
    }
    photo.setTitle(title);
    photo.setDate(new Date());
    if (!session.contains(photo))
      session.saveOrUpdate(photo);
  }

  public List<Photo> listPhotos(Session session) throws OtmException {
    Results results = session.createQuery("select p from Photo p;").execute();
    List<Photo> photos = new ArrayList<Photo>();
    while(results.next())
      photos.add((Photo) results.get(0));

    return photos;
  }
}

Saving a Photo instance

The createPhoto() method above creates a new instance of the Photo.class, sets the various property values and then calls the saveOrUpdate() method on Topaz Session. This is how POJO instances are attached to Topaz.

Notice that the method creates a new instance if it does not exist in the database or if it already exists, that instance is updated. This means Topaz will remove all existing RDF statements about the Photo.class object with the given identifier (not quite - it only removes the statements that are declared in the Photo.class declaration, namely the dc:title and dc:date) first followed by creation of new RDF statements about this subject-id. So the net result is the following statements being created in the named graph <local:///topazproject#photo> in our Mulgara database:

<%id> <rdf:type> <topaz:Photo>
<%id> <dc:title> '%title'
<%id> <dc:date> '%date'^^<xsd:dateTime>

In the above:

  • % denotes the value of the corresponding variable in the createPhoto() method.
  • aliases are expanded out.
Saving rdf:type values explained

The @Entity declaration on Photo.class specifies an rdf:type of <topaz:Photo>. So Topaz will insert this rdf:type statement every time a Photo.class instance is written to the database.

Serializing explained

Note that in the above there are three variables that are written out to the triple-store. Prior to writing out, these needs to be serialized. The %id and %title are straight forward. String representations is sufficient to write it out to the triple-store. However the %date is different. It is represented in the Photo.class instances as a java.util.Date object and the @Predicate mapping on the field denotes it as a typed literal with data-type <xsd:dateTime>. So the value needs to be converted to a String representation satisfying xsd:dateTime lexical representation. (See http://www.w3.org/TR/xmlschema-2/#dateTime)

This is done in Topaz with the help of Serializers. There is a [<insert-link> SerializerFactory?] that can be used by the application to define Serializers for various java classes. Topaz has already defined a default Serializer for java.util.Date with xsd:dateTime since it is a common enough usage.

Loading Photo instances

The listPhotos() method in the PhotoService?.class above shows an example of how Photo instances are loaded. The simplest form of a load is:

  Photo photo = session.get(Photo.class, id);

This will load all the RDF statements about id from the graph <local:///topazproject#photo> and will cause Topaz to create a Photo.class instance and set property values on it.

Instance creation explained

On a load from the store, Topaz creates an instance of the Photo.class. This is done by reflection and by using the empty argument constructor for the Photo.class.

But before it creates an instance Topaz needs to examine the statements found at the subject-uri and determine that this indeed belongs to a Photo.class. Topaz makes that determination by checking for the existence of an rdf:type statement with <topaz:Photo> as the value.

Important

If you recall, the @Entity mapping definition on Photo.class is the one that defined this rdf:type value.

Specifying an rdf:type value like this is optional. If a value is not specified there is no check done by Topaz and Topaz will always create an instance even when no statements about this id are found.

Therefore it is strongly advised that you define rdf:type values for your POJO classes. When working with existing data, that may not always be possible. In those cases, be aware that topaz will return non-null object instances always on a session.get().

De-serializing explained

Once an instance of Photo.class is created as above, the next step in Topaz's internal object-loading process is to set the values for the properties 'title' and 'date'. The @Predicate mappings will determine how these properties are set.

@Predicate on 'title' maps statements with 'dc:title' as the one that needs to be used to load the 'title' field. And similarly 'date' field is loaded from statements with 'dc:date' as the predicate-uri.

Note that in RDF, there could be multiple statements all with the same subject-uri and predicate-uri. (ie. with max-cardinality for the predicate-uri being greater than one). However from the fact that 'title' and 'date' are scalar fields, Topaz has made the determination that 'dc:title' and 'dc:date' are functional properties. So if there are more than one statement found for the 'dc:title' or 'dc:date', Topaz will throw an exception and abort the object-load operation that was requested.

Next step is to convert the serialized value returned by the triple-store into the java representation required by 'title' and 'date' fields. 'title' field requires no conversion since the field value is its lexical representation.

The 'date' field however is converted to a java.util.Date using the xsd:dateTime Serializer defined in the [<insert-link> SerializerFactory?]. This is the reverse of the process defined in the Serializing the values step above.

listPhotos() explained

The list photos employs the querying mechanism in Topaz. Topaz has an [<insert-link> Object Query Language] that allows queries to be formulated in terms of the java classes that the application is working with. The structure of the query language is something that is similar to SQL. There is a projection list, a 'from' clause and a 'where' clause.

The 'projection' list can include object instances or field values or even the result of another query (subquery). The 'from' list contains the list of entities involved in the query and finally the 'where' clause that defines the constrains.

In this example, we are interested in all instances of Photo.class. So the 'projection' list contains the variable that represents Photo.class and the 'from' list contains the name of the Photo.class entity ('Photo') and the 'where' constraints are empty.

The rest of the code in listPhotos() involves creating a query object and executing the query and iterating over the results and building a list of all Photo.class instances.

Note that all instances created by the query follows the same mechanisms described in Creating a Photo.class instance and De-serializing and setting values sections.

The PhotoServlet

We'll wire this all together finally with a simple servlet that lists all photos and allows create, update and delete of photos. The Servlet is as follows:

package org.topazproject.examples.photo;

import java.io.IOException;
import java.io.PrintWriter;

import java.net.URI;
import java.net.URISyntaxException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.topazproject.otm.OtmException;
import org.topazproject.otm.Session;
import org.topazproject.otm.SessionFactory;

public class PhotoServlet extends HttpServlet {
  private static final Log    log  = LogFactory.getLog(PhotoServlet.class);
  private SessionFactory factory;
  private PhotoService   photoService;

  @Override
  public void init() throws ServletException {
    TopazConfigurator conf = new TopazConfigurator();
    factory        = conf.getSessionFactory();
    photoService   = new PhotoService();
  }

  @Override
  public void doGet(HttpServletRequest req, HttpServletResponse resp)
             throws ServletException, IOException {
    process(req, resp);
  }

  @Override
  public void doPost(HttpServletRequest req, HttpServletResponse resp)
              throws ServletException, IOException {
    process(req, resp);
  }

  protected void process(HttpServletRequest req, HttpServletResponse resp)
                  throws ServletException, IOException {
    Session session = null;

    try {
      session = factory.openSession();
      session.beginTransaction();
      process(req, resp, session);
      session.getTransaction().commit();
    } catch (OtmException e) {
      throw new ServletException("Processing error", e);
    } finally {
      try {
        session.close();
      } catch (Throwable t) {
      }
    }
  }

  protected void process(HttpServletRequest req, HttpServletResponse resp,
      Session session) throws IOException, OtmException {
    String action = req.getParameter("action");

    if ((action == null) || "list".equals(action)) {
      respond(session, resp, null, null);
    } else {
      String id = req.getParameter("id");
      if (id != null)
        id = id.trim();
      if (id.length() == 0)
        id = null;

      if (id == null) {
        respond(session, resp, "id must be specified", "red");
      } else if ("create".equals(action) || "update".equals(action)) {
        try {
          URI uri = new URI(id);
          if (!uri.isAbsolute())
            respond(session, resp, "id must be an absolute URI", "red");
          else {
            log.info("About to " + action + " photo with id = " + id);
            photoService.createPhoto(session, uri, req.getParameter("title"));
            respond(session, resp, action + "d photo with id : " + id, "green");
          }
        } catch (URISyntaxException e) {
          respond(session, resp, "id must be a valid URI", "red");
        }
      } else if ("delete".equals(action)) {
        log.info("About to " + action + " photo with id = " + id);
        Photo photo = session.get(Photo.class, id);

        if (photo != null)
          session.delete(photo);

        respond(session, resp, action + "d photo with id : " + id, "green");
      } else {
        respond(session, resp, "unknown action '" + action + "'", "red");
      }
    }
  }

  protected void respond(Session session, HttpServletResponse resp,
      String message, String color) throws IOException, OtmException {
    PrintWriter out = resp.getWriter();
    out.println("<html><head><title>Photo Manager</title></head><body>");

    out.println("<h2>Photo Manager</h2>");
    if (message != null)
      out.println("<b><i><font color='" + color + "'>"  + message 
                  + "</font></i></b>");

    out.println("<table border='2'>");
    out.println("<tr><th>id</th><th>title</th><th>action</th></tr>");
    out.println("<tr><form method='post'>");
    out.println("<td><input name='id'></td><td><input name='title'></td>");
    out.println("<td><input type='submit' name='action' value='create'></td>");
    out.println("</form></tr>");

    for (Photo photo : photoService.listPhotos(session)) {
      out.println("<tr><form method='post'>");
      out.println("<td><input name='id' type='hidden' value='" + photo.getId() + "'>"
                  + photo.getId() + "</td>");
      out.println("<td><input name='title' value='" + photo.getTitle() + "'></td>");
      out.println("<td><input type='submit' name='action' value='update'>");
      out.println("<input type='submit' name='action' value='delete'></td>");
      out.println("</form></tr>");
    }

    out.println("</table></body></html>");

    out.flush();
  }
}

With the corresponding updated web.xml:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Photo Manager Web Application</display-name>

  <servlet>
    <servlet-name>Photo Manager</servlet-name>
    <servlet-class>org.topazproject.examples.photo.PhotoServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>Photo Manager</servlet-name>
    <url-pattern>/photoManager</url-pattern>
 </servlet-mapping>

</web-app>

You can now run this using 'mvn jetty:run-war' and point your browser to http://localhost:8080/topaz-photo-example/photoManager

PhotoServlet explained

init() method

This does the initialization for Topaz and creates a Topaz SessionFactory?. Also a PhotoService? instance is created to work with Photo objects.

Note that the TopazConfigurator? object is discarded after extracting the SessionFactory?. In a more complex application, the TopazConfigurator? may act as a factory for the SessionFactory? object.

process() method

Notice all that it does is to ensure that a Session is created and a transaction started before delegating for further processing. This is an example of an open-session-in-view paradigm. So it is possible to have this portion in a Filter as in http://static.springframework.org/spring/docs/2.5.x/api/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.html

The other process() method does all the work of taking request parameters and executing commands on PhotoService?. The commands are 'create', 'update', 'delete' and 'list'.

Running it

You can get the complete source code for this example from topaz-photo-example.zip.

'mvn jetty:run-war' will run this. The following screen shot shows the browser window showing the PhotoServlet in action:

Photo Manager Screenshot

Attachments