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"
    }