Porting seam hotel booking example to OGM

Purpose:

 

This article documents the process of porting the seam 3 hotel booking example to use Hibernate OGM with MongoDB as the datastore.

 

Code:

 

The code is available at: https://github.com/ajf8/seam-booking-ogm

 

Note that this will need the parent 'examples' pom and the toplevel seam pom below that for the maven build to work. See below.

 

 

 

 

Environment:

 

  • JBoss Enterprise Application Platform 6 beta. These instructions should work for the community AS 7.
  • I'm using the MongoDB OGM dialect, but these instructions should apply to the infinispan or ehcache backends with some minor changes to the configuration.
  • This is based on the booking example in the seam github at the time of writing. The 3.1.0.Final example had issues.
  • Using Hibernate OGM git master at the time of writing (the mongo dialect has not been released yet).

 

Instructions:

 

  • Install MongoDB. No real configuration is required on Fedora - just 'yum install mongodb-server' and 'service mongod start'.

 

  • Clone the OGM sources, build and install into the local maven repository.

 

[alan@ajf-rh jboss]$ git clone https://github.com/hibernate/hibernate-ogm.git

[alan@ajf-rh jboss]$ cd hibernate-ogm/

[alan@ajf-rh hibernate-ogm]$ mvn clean install -Pmongodb -s settings-example.xml

 

  • Extract the EAP (or AS7) distribution:

 

[alan@ajf-rh jboss]$ unzip jboss-eap-6.0.0.Beta1.zip

 

  • Create a directory for the OGM module.

 

[alan@ajf-rh jboss]$ cd jboss-eap-6.0/modules/org/hibernate/

[alan@ajf-rh hibernate]$ mkdir ogm

[alan@ajf-rh hibernate]$ cd ogm/

 

  • Copy OGM core, the dialects, and the MongoDB java driver into this module directory.

 

[alan@ajf-rh ogm]$ cp ~/jboss/hibernate-ogm/hibernate-ogm-core/target/hibernate-ogm-core-4.0.0-SNAPSHOT.jar .

[alan@ajf-rh ogm]$ cp ~/jboss/hibernate-ogm/hibernate-ogm-infinispan/target/hibernate-ogm-infinispan-4.0.0-SNAPSHOT.jar .

[alan@ajf-rh ogm]$ cp ~/jboss/hibernate-ogm/hibernate-ogm-mongodb/target/hibernate-ogm-mongodb-4.0.0-SNAPSHOT.jar .

[alan@ajf-rh ogm]$ cp ~/.m2/repository/org/mongodb/mongo-java-driver/2.7.2/mongo-java-driver-2.7.2.jar .

 

  • Create the module.xml (in the same ogm module directory). Ideally we would probably split the dialects into separate modules, but lets keep it simple.

 

https://raw.github.com/ajf8/seam-booking-ogm/master/module.xml

 

<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="org.hibernate" slot="ogm">
    <resources>
        <resource-root path="hibernate-ogm-core-4.0.0-SNAPSHOT.jar"/>
        <resource-root path="hibernate-ogm-mongodb-4.0.0-SNAPSHOT.jar"/>
        <resource-root path="hibernate-ogm-infinispan-4.0.0-SNAPSHOT.jar"/>
        <resource-root path="mongo-java-driver-2.7.2.jar"/>
    </resources>

    <dependencies>
        <module name="org.jboss.as.jpa.hibernate" slot="4"/>
        <module name="org.hibernate" slot="main" export="true" />
        <module name="javax.api"/>
        <module name="javax.persistence.api"/>
        <module name="javax.transaction.api"/>
        <module name="javax.validation.api"/>
        <module name="org.infinispan"/>
        <module name="org.javassist"/>
        <module name="org.jboss.logging"/>
    </dependencies>
</module>

 

 

 

  • Clone the seam booking example from the seam github. There was an issue with the 3.1.0.Final example at the time of writing. Maybe a future release will be ok.

 

[alan@ajf-rh jboss]$ git clone https://github.com/seam/parent.git seam

[alan@ajf-rh jboss]$ cd seam/

[alan@ajf-rh seam]$ git clone https://github.com/seam/dist.git

[alan@ajf-rh seam]$ git clone https://github.com/seam/examples.git

[alan@ajf-rh seam]$ cd examples/

[alan@ajf-rh examples]$ cp -r booking/ mongobooking

[alan@ajf-rh examples]$ cd mongobooking/

 

If you don't feel like doing the refactorings and just want to try it out, you should be able to clone the project from my github into the examples directory.

 

  • Add the OGM, OGM dialect and Hibernate search dependencies to the pom.xml

 

https://raw.github.com/ajf8/seam-booking-ogm/master/pom.xml

 

<dependency>
    <groupId>org.hibernate.ogm</groupId>
    <artifactId>hibernate-ogm-core</artifactId>
    <version>4.0.0-SNAPSHOT</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.hibernate.ogm</groupId>
    <artifactId>hibernate-ogm-infinispan</artifactId>
    <version>4.0.0-SNAPSHOT</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.hibernate.ogm</groupId>
    <artifactId>hibernate-ogm-mongodb</artifactId>
    <version>4.0.0-SNAPSHOT</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-search</artifactId>
    <version>4.1.0.Final</version>
</dependency>

 

 

  • Create an empty directory for lucene indexes, writable by the application server process:

 

[alan@ajf-rh mongobooking]$ mkdir ~/.luceneindexes

 

  • Update persistence.xml. Note the lucene index directory will need changing.

 

src/main/resources-jbossas7/WEB-INF/classes/META-INF/persistence.xml

 

https://raw.github.com/ajf8/seam-booking-ogm/master/src/main/resources-jbossas7/WEB-INF/classes/META-INF/persistence.xml

 

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">
  <persistence-unit name="booking">
    <class>org.jboss.seam.examples.booking.model.User</class>
    <class>org.jboss.seam.examples.booking.model.Booking</class>
    <class>org.jboss.seam.examples.booking.model.Hotel</class>
    <properties>
      <property name="hibernate.transaction.jta.platform" value="org.hibernate.service.jta.platform.internal.JBossAppServerJtaPlatform"/>
      <property name="jboss.as.jpa.providerModule" value="org.hibernate:ogm"/>
      <!-- Properties for Hibernate (default provider for JBoss AS) -->
      <property name="hibernate.ogm.datastore.provider" value="org.hibernate.ogm.datastore.mongodb.impl.MongoDBDatastoreProvider"/>
      <property name="hibernate.ogm.mongodb.database" value="booking"/>
      <property name="hibernate.ogm.mongodb.host" value="localhost"/>
      <property name="hibernate.search.default.directory_provider" value="filesystem"/>
      <property name="hibernate.search.default.indexBase" value="/home/alan/.luceneindexes"/>
    </properties>
    <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
  </persistence-unit>
</persistence>

 

 

  • OGM does not currently support criteria queries, which the booking example uses. There are two queries in the example - the action which searches for hotels, and the one which finds a users bookings. Create an eclipse project and then import it to make the code changes.

 

[alan@ajf-rh mongobooking]$ mvn eclipse:eclipse

 

 

  • Create this class, org.jboss.seam.examples.booking.cdi.FMEMProvider. This allows a FullTextEntityManager to be injected into our action classes.

 

https://github.com/ajf8/seam-booking-ogm/blob/master/src/main/java/org/jboss/seam/examples/booking/cdi/FTEMProvider.java

 

package org.jboss.seam.examples.booking.cdi;

import javax.enterprise.inject.Produces;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.Search;

/**
 * @author Emmanuel Bernard
 */
public class FTEMProvider {
        @PersistenceContext
        EntityManager em;

        @Produces
        public FullTextEntityManager getFTEM() {
                return Search.getFullTextEntityManager( em );
        }
}

 

 

  • We need to add @Indexed annotations to the entities which need indexing by Hibernate Search. @Field on the fields which need indexing, and @IndexedEmbedded to create the embedded index allowing us to search on booking.user.

 

Booking entity:

 

https://raw.github.com/ajf8/seam-booking-ogm/master/src/main/java/org/jboss/seam/examples/booking/model/Booking.java

 

@Entity
@Veto
@Indexed
public class Booking implements Serializable {
    private Long id;
    @IndexedEmbedded
    private User user;

 

 

Hotel entity:

 

https://raw.github.com/ajf8/seam-booking-ogm/master/src/main/java/org/jboss/seam/examples/booking/model/Hotel.java

 

@Entity
@Table(name = "hotel")
@Veto
@Indexed
public class Hotel implements Serializable {
    private Long id;
    @Field
    private String name;
    @Field
    private String address;
    @Field
    private String city;
    @Field
    private String state;
    @Field
    private String zip;
    @Field
    private String country;

 

 

User entity:

 

https://raw.github.com/ajf8/seam-booking-ogm/master/src/main/java/org/jboss/seam/examples/booking/model/User.java

 

@Entity
@Table(name = "traveler")
@Veto
@Indexed
public class User implements Serializable {
    private static final long serialVersionUID = -602733026033932730L;
    @Field
    private String username;

 

 

  • Refactor the HotelSearch class to use hibernate search. Start by adding the FullTextEntityManager (which is injected by the FMEMProvider defined earlier using CDI/weld).

 

https://github.com/ajf8/seam-booking-ogm/raw/master/src/main/java/org/jboss/seam/examples/booking/inventory/HotelSearch.java

 

@Inject
private Provider<FullTextEntityManager> lazyFEM;

 

 

  • Replace the queryHotels method with one which uses Hibernate search and lucene. You could experiment with the lucene queries here.

 

private void queryHotels(final SearchCriteria criteria) {
    if (criteria.getQuery().length() < 1) {
        nextPageAvailable = false;
        hotels = new ArrayList<Hotel>();
        return;
    }

    FullTextEntityManager em = lazyFEM.get();

    final QueryBuilder builder = em.getSearchFactory().buildQueryBuilder()
            .forEntity(Hotel.class).get();

    final Query luceneQuery = builder.keyword()
        .onField("name").andField("address").andField("city")
        .andField("state").andField("zip").andField("country")
                .matching(criteria.getQuery()).createQuery();

    System.out.println(luceneQuery.toString());

    final FullTextQuery query = em.createFullTextQuery(luceneQuery,
            Hotel.class);
    query.initializeObjectsWith(ObjectLookupMethod.SKIP,
            DatabaseRetrievalMethod.FIND_BY_ID);

    final List<Hotel> results = query
            .setFirstResult(criteria.getFetchOffset())
            .setMaxResults(criteria.getFetchSize()).getResultList();

    nextPageAvailable = results.size() > criteria.getPageSize();
    if (nextPageAvailable) {
        // NOTE create new ArrayList since subList creates unserializable
        // list
        hotels = new ArrayList<Hotel>(results.subList(0,
                criteria.getPageSize()));
    } else {
        hotels = results;
    }

    log.info(messageBuilder
            .get()
            .text("Found {0} hotel(s) matching search term [ {1} ] (limit {2})")
            .textParams(hotels.size(), criteria.getQuery(),
                    criteria.getPageSize()).build().getText());
}

 

 

  • A similar is refactoring is needed for the BookingHistory class. Add the FullTextEntityManager (as per above), then replace the fetchBookingsForCurrentUser method

 

https://github.com/ajf8/seam-booking-ogm/raw/master/src/main/java/org/jboss/seam/examples/booking/booking/BookingHistory.java

 

private void fetchBookingsForCurrentUser() {
    String username = currentUserInstance.get().getUsername();
    FullTextEntityManager em = lazyFEM.get();

    final QueryBuilder builder = em.getSearchFactory().buildQueryBuilder()
            .forEntity(Booking.class).get();

    final Query luceneQuery = builder.keyword().onField("user.username")
            .matching(username).createQuery();

    System.out.println(luceneQuery.toString());

    final FullTextQuery query = em.createFullTextQuery(luceneQuery.
            Booking.class);
    query.initializeObjectsWith(ObjectLookupMethod.SKIP,
            DatabaseRetrievalMethod.FIND_BY_ID);

    bookingsForUser = query.getResultList();
}

 

 

  • Build the war and deploy it to the application server.

 

[alan@ajf-rh mongobooking]$ mvn clean package

[alan@ajf-rh mongobooking]$ cp target/seam-booking.war ~/jboss/jboss-eap-6.0/standalone/deployments/

 

  • Start the application server. I found there are a lot of harmless exceptions caused by CDI/weld, which can be ignored.

 

[alan@ajf-rh mongobooking]$ cd ~/jboss/jboss-eap-6.0/bin

[alan@ajf-rh bin]$ ./standalone.s

 

 

Note that when you search for hotels, unlike the original example, this only matches full keywords (case insensitive). Try typing "hotel" or "marriott"

 

  • After the application has been started and seed data has been imported, we can see entities in the MongoDB:

 

[alan@ajf-rh mongobooking]$ mongo booking
MongoDB shell version: 2.0.2
connecting to: booking
> db.traveler.findOne()
{
    "_id" : "shane",
    "email" : "shane@example.com",
    "name" : "Shane Bryzak",
    "password" : "brisbane",
    "username" : "shane"
}