Monday, May 24, 2010

VinWiki Part 1: Building an intelligent Web app using Seam, Hibernate, RichFaces, Lucene and Mahout

Introduction

This is the first post in a four part series about a wine rating and recommendation Web application, named VinWiki, built using open source technology. The purpose of this series is to document key design and implementation decisions, which may be of interest to anyone wanting to build an intelligent Web application using Java technologies. The end result will not be a 100% functioning Web application, but will have enough functionality to prove the concepts. Specifically, here is a basic roadmap of the concepts covered in each post:

  1. Introduction (May 24, 2010): Covers project setup, primary domain objects, and basic UI constructs such as server-side pagination, dynamic menus, and bookmarkable URLs with JSF / RichFaces / Facelets.
  2. Full-text Search (June 14, 2010): Implements full-text search using Hibernate Search with Lucene 2.9.2.
  3. Integrating with the Web (July 8, 2010): Authentication and registration using Facebook Connect and sharing/liking bookmarkable URLs on Facebook.
  4. Recommendations (Sept 23, 2010): Provide wine recommendations using Apache Mahout.
The idea for VinWiki was born out of two interests in my life: wine and collective intelligence. I'm a wine connoisseur and love red wines from Piedmont (Italy) and the Dry Creek and Alexander Valley's in Sonoma. I don't, however, drink expensive wine. Rather, I'm always on the lookout for that $10-15/bottle wine that just tastes good (who isn't right?) My strategy is to know a lot about a little. For example, I know the major varietals in Piedmont, their characteristics, which years were good and which were not, etc. I know these things because I want to be able to drink great inexpensive wines and help my friends do the same. Wouldn't it be nice if you had access to all this knowledge in my head the next time you go out for Italian food in North Beach?

One of my other interests is figuring out how to harvest intelligence from the interactions and contributions of users on a Web site and then apply that intelligence to improve the personal experience with the application as well as the application as a whole. The science behind this is called Collective Intelligence. To keep things simple, I consider a Web application to be "intelligent" if it has the following key ingredients:

  1. automatically improves its capabilities as more users contribute to it,
  2. scalable (machine learning algorithms do better with large amounts of data),
  3. mashable (simple Web services that expose data and services to other applications), and
  4. doesn't pretend to be more than it is!
There's a wealth of knowledge available about how to implement the first ingredient using machine learning. And, as we'll see in the 4th post in this series, there is a wonderful open source project named Mahout that provides scalable implementations of many popular machine learning algorithms, such as clustering, classification, and recommendations. However, please keep in mind that it takes rigorous testing and experimentation to ensure these algorithms produce good results for your data. Don't assume that because an algorithm sounds sophisticated that it will produce good results in your application. Hence, the last key ingredient! In this application, intelligence comes in the form of making recommendations for wines from previous ratings using collaborative filtering. Presumably, as more users contribute wines and ratings, the application will improve for all users.

Toolset
The solution is built using the following open-source technologies:

Recommended Reading List
Here are a few excellent books that introduce you to the subject of intelligent Web applications: If you are interested in full-text search, the following two books are invaluable assets:

Basic Requirements
There are three primary activities you can do with this application:
  1. Rate wines you've already tried
  2. Find and read ratings for wines you may like to try
  3. Receive recommendations of new wines you should to try based on your previous ratings
These activities equate to the following use cases: Here is a screen shot of the home page (note: best I could do with the image quality since blogger limits the width to 800).
Home page

Domain Objects
The following UML Class Diagram depicts the primary domain objects in our model.
UML Class Diagram for the org.vinwiki.model package
JPA EntityDescription
RegionEncapsulates information about a geographical area that produces wine, such as Bordeaux. Implements the org.vinwiki.model.Composite interface to represent a hierarchical tree of regions and sub-regions.
VarietalEncapsulates information about grape variety used to make wine, such as Chardonnay.
VarietalPctRepresents the percentage of a varietal in a specific wine.
WineryEncapsulates information about a wine producer, such as Robert Mondavi. A Winery may have a latitude and longitude specified, which would allow us to show the winery on map of wineries in a region.
WineEncapsulates information about a specific wine, uniquely identified by name, winery, and vintage (typically the year the grapes were harvested).
UserEncapsulates information about a registered user in the application.
PreferencesEncapsulates user-supplied settings used to personalize the application.
TagHolds information for a keyword created by a user for rating a wine.
UserTagAssociates a tag with a user's rating.
RatingRepresents a specific user's rating (score and comments) of a specific wine.
Relational Database Model
Here is the Hibernate model translated into a MySQL database model:
Relational Database Model
Seed Data
Since an application like this isn't very interesting without some real data, I extracted some wine-related entities from Freebase using their RESTful Web Services API, along with some manual tweaking to better fit the desired data model. Specifically, the seed data contains:
  • 104 wine regions / sub-regions
  • 487 grape varietals
  • 772 wineries
  • 2,025 wines
So this should get us started with some real data, but as we'll see below, users will also be able to add new wines as needed.

Persistence Annotations
Most of the JPA annotations in the domain model are straight-forward, so I'll refer you to the source code for further study. However, I'm using a few annotations that warrant further discussion, including:

Hierarchical Regions (composite pattern)
A wine Region can have sub-regions, such as France has Bordeaux and Alsace. JPA makes modeling composites really easy using @ManyToOne with @JoinColumn:
@ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_region", nullable = true)
    public Region getParentRegion() {
        return this.parentRegion;
    }
Yep! It really is that easy ...
@javax.persistence.MappedSuperclass
Entities typically share a common set of fields, such as ID, name, description so it is common to encapsulate these common fields into a base class. The javax.persistence.MappedSuperclass annotation designates a class whose mapping information is applied to the entities that inherit from it. As you can see from the class diagram, the org.vinwiki.model.AbstractItemBase class serves as the @MappedSuperclass in our model.

@org.hibernate.annotations.Cache
If you think about it, most of the wine-related entities in this model are primarily read-only in nature. The main user activity in this application is to add ratings to existing wine objects. Thus, the Wine, Winery, Varietal, and Region entities are good candidates for caching in what is called the second-level cache. We use the @org.hibernate.annotations.Cache annotation to define the cache concurrency strategy for each entity we want to cache. I'm using org.hibernate.annotations.CacheConcurrencyStrategy.NONSTRICT_READ_WRITE because the application may need to update these entities occasionally, but in most cases they are used as read-only objects. For more information about cache concurrency strategy, please refer to The Second Level Cache section in the Hibernate Core documentation.

Of course, we also need to enable the second-level cache in persistence.xml. I found it easiest to use Ehcache initially but you should research the other providers, e.g. JBoss Cache, to determine the most appropriate solution for your application. I'll revisit this decision when I tackle clustering this application in the cloud. For now, here are the salient properties specified in persistence.xml:
<property name="hibernate.cache.provider_class" value="net.sf.ehcache.hibernate.EhCacheProvider"/>
  <property name="hibernate.cache.use_query_cache" value="true"/>
  <property name="hibernate.cache.use_second_level_cache" value="true"/>
  <property name="hibernate.cache.region_prefix" value=""/>
  <property name="hibernate.generate_statistics" value="true"/>
  <property name="hibernate.session_factory_name" value="SessionFactories/vinwikiSF"/>
Notice that I'm enabling Hibernate Statistics using the hibernate.generate_statistics property. During startup, I deploy the Hibernate StatisticsService MBean using:
hibernateMBeanName = new ObjectName("Hibernate:type=statistics,application=vinwiki");
    StatisticsService mBean = new StatisticsService();
    mBean.setSessionFactoryJNDIName("SessionFactories/vinwikiSF");
    ManagementFactory.getPlatformMBeanServer().registerMBean(mBean, hibernateMBeanName);
I also use Ehcache as the Seam cache provider, in resources/WEB-INF/components.xml:
<cache:eh-cache-provider/>
This will come in handy when we start displaying tag clouds and other expensive data structures to our users.
Be sure to include ehcache.jar in your deployed-jars.list file!

Solutions
In this section, I walk you through the solutions to each use case described above.

Register New User Account
For this requirement, I leveraged the solution from my previous blog post. We'll see in post #3 in this series how to allow Facebook users to automatically register and authenticate using their Facebook account. However, I did make a few minor improvements to the handling of user preferences so be sure to review the code in the org.vinwiki.user package after reading the aforementioned blog post.

Basic Navigation
There are a number of ways a user can browse for wines in the application, including by region, most recent, best rated, as well as search. Moreover, a user can switch between these mechanisms at any time. The nav component (org.vinwiki.action.Nav) deployed in session scope supports user navigation.

The nav component can be in one of two states during its lifecycle: A) guest mode when #{identity.loggedIn} is false or B) authenticated user mode when #{identity.loggedIn} is true. This is because Seam does not create a new session component after a user is authenticated.

When a new session is created, we need to generate a default view of the wine data; a list of the Most Recent Wines added to the application seems like a good choice. I use Seam's @Factory annotation to provide the default view (the Seam documentation calls this pull-style MVC):
@Factory("mainMenu")
    public void initMainMenu() {
        if (mainMenu == null) {
            mainMenu = buildMainMenu();
        }
        // Ensure the current PagedDataFetcher is in-sync with the current request
        syncDataFetcherWithRequest();
    }
To handle the switch from state A to B, nav observes the Identity.EVENT_POST_AUTHENTICATE event:
@Observer(Identity.EVENT_POST_AUTHENTICATE)
    public void postAuthenticate(Identity identity) {
        cleanupDataProvider();
        // after login, display the user menu instead of the guest menu
        mainMenu = getMainMenu();
        syncDataFetcherWithRequest();
    }
Dynamic RichFaces PanelMenu
One method of navigating is to browse wines by region. Recall that Region implements the org.vinwiki.model.Composite interface so we can compose a hierarchical tree structure of regions. I chose the RichFaces PanelMenu for this example, but you could also adapt the code to build another type of menu. Menu items are dynamic so we need to programmatically build a org.richfaces.component.html.HtmlPanelMenu using the RichFaces API. Because the menu is a form of navigation, the nav component builds the menu. On the home page, we have a <rich:panelMenu> tag with the binding set to "mainMenu".
<a4j:form prependId="false">
    <rich:panelMenu id="mainMenu" binding="#{mainMenu}"/>
  </a4j:form>
Seam invokes the initMainMenu method of our nav component to resolve the binding. All guests share the same menu, but each user can have their own menu. You could imagine allowing users to hide specific regions they are not interested in, which will impact the rendering of the main menu. One caveat is that you should manually set the ID for all the UI components your binding creates or you will most likely encounter duplicate ID exceptions, especially after hot deployments.

Bookmarkable Navigation URLs
It would be nice if the application allowed users to bookmark a specific filter like "Most Recent" or region like "California". For example, in the screen shot below, notice that the URL in the browser address bar reflects the fact that the user clicked on California in the main menu. The user can bookmark this page in their browser to instantly view the list of Californian wines available in VinWiki.
Bookmarkable URL
Seam makes this possible using URL re-writing and page parameters. My solution is largely based on the Blog example provided in the Seam documentation. When the menu was constructed, I set the action for the California region menu item to be /region.xhtml?r=California. In pages.xml, I link the "r" request parameter to the region property of an event-scoped component bookmarkable, see org.vinwiki.action.BookmarkableRequestInfo. In addition, I use a rewrite rule to make the URL more intuitive (/region/California instead of /region.seam?r=California):
<page view-id="/region.xhtml">
    <rewrite pattern="/region/{r}"/>
    <param name="r" value="#{bookmarkable.region}"/>
  </page>
This is where the aforementioned syncDataFetcherWithRequest method comes into play; this method uses the injected bookmarkable component to determine which data to show to the user, which in this case is the list of Californian wines.

One drawback to allowing navigation requests to be bookmarked in this manner is that I had to duplicate the contents of view/home.xhtml into filter.xhtml because it seems that adding multiple rewrite patterns on a single page leads to some weird URLs. I'm sure this could be overcome with some work, but since I'm using Facelets, the amount of duplication is minimal (and of course the UI is not final so the filter page may end up being different anyway). In the next posting, I'll show you how to make search results bookmarkable as well.

Server-side Pagination
Server-side pagination is essential for gracefully handling a large number of objects. The basic idea is to only display a small subset of the total results to the user at one time. The key, of course, is to extend the sub-set concept to the server and only load small sub-set of data from the database at a time. Only doing client-side pagination will become a major performance issue as the number of objects in your database increases. Thankfully, RichFaces does most of the work for us! Here is a screenshot of VinWiki's scrollable data table backed by server-side pagination:
scrollable data table screenshot
And, the corresponding JSF syntax (from view/WEB-INF/facelets/itemTable.xhtml:
scrollable data table JSF syntax
  1. My solution is based largely on the sample code provided with RichFaces (see org.richfaces.demo.extendeddatamodel.AuctionDataModel). Essentially, my <rich:dataTable> interacts with an event-scoped component navDataModel that extends org.ajax4jsf.model.SerializableDataModel. Under the covers, the DataModel (see org.vinwiki.action.PagedDataModelBase) component does everything needed to support AJAX-driven pagination except the actual loading of data. To load data, the DataModel delegates to a org.vinwiki.action.PagedDataProvider, whose implementation is a session-scoped component that loads and caches items from the database.
  2. Under the covers, the number of rows displayed to the user per page comes from the user's preferences.
  3. The RichFaces <rich:datascroller> provides the paging mechanism for our paged data table.
Here is a UML class diagram showing the relationship between the DataModel and DataProvider (most of these classes can be used in your project as they have nothing to do with Wine):
UML Class Diagram of server-side pagination
As you might expect, the concrete implementation of org.vinwiki.action.PagedDataProvider is our handy nav component which extends org.vinwiki.action.PagedDataProviderBase. PagedDataProviderBase does most of the work except the actual fetching of a page of data from the database, which is provided by a org.vinwiki.action.PagedDataFetcher. There is a concrete implementation of org.vinwiki.action.PagedDataFetcher for each type of navigation. For example, the org.vinwiki.action.FetchRegion class provides Wine objects for a specific region to the data provider. Notice that my current PagedDataFetcher implementations are *not* Seam components and expect the EntityManager and User ID to be passed to them. I chose this approach to make it really easy to build new types of navigation queries; all you have to do is provide a query to return a page of data and a query to return the total number of items available to the current user.

The PagedDataProviderBase maps each starting index to a List of Item objects. A Map is a good solution if you are using the <rich:datascroller> because the user can jump from page 1 to 10 without going through pages 2-9 first. If you're using a sequential paging mechanism then a simple List should suffice. Of course the Map could grow very big if the user visits many pages in the scroller. I'll leave it as an exercise for the reader to solve using SoftReferences.

View Wine Details
Assuming the user finds a wine of interest while browsing the application, they may want to view more information for the wine as well as scroll through other users' ratings. wine details screenshot
You didn't know the Rat Pack liked wine and wrote Latin did you ;-)

There are a number of possible activities the wine details view can offer the user, including:
  • Add / edit rating
  • View a list of similar wines (MoreLikeThis)
  • Contextual display Ads
  • Navigation controls to view the next wine in the results
  • Edit information about the wine itself
Consequently, it makes sense to implement a new page for viewing wine details ( view/wine.xhtml ). From the home page, we take the user to the wine details page using a Seam <s:link> tag, which allows us to pass the Wine ID as a request parameter:
<s:link view="/wine.xhtml" value="#{item.fullName}" style="font-size:14px;">
    <f:param name="wid" value="#{item.id}"/>
  </s:link>
In pages.xml, I map a request parameter to the wineId property of the viewWine component:
<page view-id="/wine.xhtml">
    <rewrite pattern="/wine/{wid}"/>
    <param name="wid" value="#{viewWine.wineId}"/>
  </page>
Notice that I'm also re-writing the URL. Link the "wid" parameter to #{viewWine.wineId} begins a new conversation for viewing the wine:
@Begin(flushMode = FlushModeType.MANUAL, join = true)
    public void setWineId(Long wineId) {
       ...
    }
There are a number of ways to begin a conversation so be sure to research the best approach for your application in the Seam documentation.
The wine details page can be bookmarked as well. In this case, however, I'm using the push-style MVC approach discussed in the Seam documentation. I had to get clever with the Home link on the details page because if the user comes through a bookmark, history.go(-1) won't work for returning the user to the VinWiki home page. Thus, I rely on an AJAX call to end the conversation and then invoke the backToHome JavaScript function:
<a4j:commandLink value="#{messages.backToHome}" action="#{viewWine.endViewWine()}" oncomplete="backToHome()" style="font-size:14px;"/>
From view/layout/template.xhtml:
function backToHome() {
    var currentHost = document.location.protocol+"//"+document.location.host;
    if (document.referrer && document.referrer.startsWith(currentHost)) {
        history.back();
    } else {
        window.location.replace("/vinwiki/");
    }
}


Add Rating for Existing Wine
There are two ways a user can add a rating:
  1. lookup a wine on-the-fly from the home page, or
  2. view details for a specific wine and then add the rating from the wine details view.
Rating with on-the-fly Lookup
For authenticated users, the header menu on the home page includes a link to "Add Rating". Clicking on the Add Rating link, triggers the #{ratingHome.beginRatingWine()} action using AJAX. When the action completes, the addRatingPanel is shown to the user.
Add rating from Home Page, requires lookup of wine on-the-fly.
From view/WEB-INF/facelets/headerControls.xhtml
<a4j:commandLink value="Add Rating" action="#{ratingHome.beginRatingWine()}" reRender="addRatingPanel"
            oncomplete="#{rich:component('addRatingPanel')}.show()" styleClass="hdrLink"/>
The conversational ratingHome component (org.vinwiki.action.RatingHome based on Seam's Application Framework) manages adding and editing Rating objects by users.

So now let's see how on-the-fly lookup works ... from view/WEB-INF/facelets/addRating.xhtml
JSF syntax for auto-complete on wine field.
  1. Decorate the form field as a required field using Seam's <s:decorate> tag and a Facelets template. If a validation error occurs, Seam will decorate the field with error information.
  2. When the "onblur" event fires on the input field, send an AJAX request to the server to determine if the user selected a known wine.
  3. Attach a RichFaces suggestion box <rich:suggestionbox> to the input field to auto-complete the wine name as the user types. On the server, the RichFaces suggestion box invokes the #{ratingHome.autoCompleteWine} action. I'll discuss the RichFaces request queue in more detail below, but for now, you should realize that it is dangerous to have an auto-complete component without some sort of request flood control in place.
So assuming the user has successfully filled-in the rating form, let's see what happens when the form is submitted:
JSF syntax to submit the add rating form.
There's a fair amount of complex machinery going on in this one tag! Let's analyze it step-by-step:
  1. A RichFaces <a4j:commandButton> submits the form using AJAX to invoke the #{ratingHome.saveRating} action within the same conversation started when the user clicked on Add Rating.
  2. The reRender attribute tells RichFaces to re-render the addRatingPanel and itemTable components in the component tree and updated in the browser DOM after the AJAX response is completed. This ensures that the panel will display any errors that occur on the server.
  3. If there are no errors, close the modal panel. Notice that I'm calling the hasJsfErrMsg JavaScript function, which returns true if there are any error messages queued in the FacesContext. The hasJsfErrMsg function is defined in my main Facelets layout template (view/layout/template.xhtml) and relies on an <a4j:outputPanel> to update the value of a hidden field with ID 'jsfMsgMaxSev' on every AJAX request. The value for this field is pulled from the FacesContext.getMaximumSeverity() method.
    <a4j:outputPanel ajaxRendered="true">
        <a4j:form prependId="false">
          <h:inputHidden id="jsfMsgMaxSev" value="#{jsf.maxMsgSev}"/>
        </a4j:form>
      </a4j:outputPanel>
    
Rating from Details
When a user views a wine, a new conversation is started with the viewWine component.
Add rating from wine details, wine to rate is already selected.
Recall that we are already in a conversation when viewing the wine. Thus, the Add Rating operation will occur in a nested conversation using the ratingHome component.
@Begin(nested=true, flushMode=FlushModeType.MANUAL)
    public void beginRatingWine(Wine currentWine) {
        // attach the objects needed to create a rating to the
        // extended PersistenceContext for this conversation
        user = getEntityManager().merge(currentUser);
        wine = getEntityManager().find(Wine.class, currentWine.getId());
        updateStateForWine();
        info("User {0} has starting rating wine {1} in NESTED conversation {2}.", user.getUserName(), wine.getFullName(), Conversation.instance().getId());
    }
Upon starting the nested conversation, you need to "attach" the User and Wine objects to the extended PersistenceContext (entityManager).

Add New Wine
What type of wiki would this be if user's could not add new content on-the-fly? Of course, this application is not a full-featured wiki (see the wiki project in the Seam examples), but any authenticated user can add a new Wine (and dependent objects like Winery and Region) to the application. The wineHome component does most of the heavy lifting for adding a new Wine to the system, see org.vinwiki.action.WineHome. WineHome provides persistence operations for Wine entities by extending org.jboss.seam.framework.EntityHome from the Seam Application Framework. The implementation is mostly uninteresting from a re-use perspective, with the exception of being a @Factory for a component named wine.
@Factory("wine")
    public Wine initWine() {
        return getInstance();
    }
This allows EL expressions in /admin.xhtml to refer to the new Wine object simply as wine, such as #{wine.type}. The wine is managed in conversation scope along with wineHome.
We'll need to address the threat of spam and cross-site scripting (XSS) hacks before we can release this application on the Web, which I'll address in post #3.


Edit Wine
Edit wine also uses the wineHome component and /admin.xhtml.

So that about covers the specific use cases for this posting. Now, let's look at some specific UI implemenation details that will help you build better apps with Seam and RichFaces.

Other Useful UI Implementation Details
Facelets
I'm using Facelets for templating and UI component re-use. The main layout template is view/layout/template.xhtml. Each view references this template in the root Facelets <ui:composition ... template="layout/template.xhtml"> element.

RichFaces Semantic Layouts
I've started experimenting with the semantic layouts support provided by RichFaces (see view/layout/template.xhtml). I think the layouts help reduce CSS you need to manage for application so their definitely worth checking out.

RichFaces ModalPanel, Forms, and Seam Conversations
The RichFaces ModalPanel component is an excellent tool for allowing the user to perform a quick operation without losing their current context with your application. For example, the Add Rating ModalPanel allows the user to add a rating to the wine they are currently viewing without going to a separate page. Unfortunately, ModalPanel can also be major source of frustration if not handled correctly, especially when working with an AJAX-driven form on the panel. In this section, I hope to spare you some of that frustration by tackling some of the troublesome areas you may encounter when working with ModalPanels.
Opening the ModalPanel (and beginning a Conversation)
For the complete code for this section, please see view/WEB-INF/facelets/userPreferences.xhtml.

JSF syntax to open a ModalPanel.
Here are some tips to keep in mind when opening a ModalPanel within a conversation:
  1. The reRender attribute should include the ID of the panel you are opening. This ensures the UI components in the panel reflect the state of the conversation.
  2. When the action completes, use JavaScript to show the panel via oncomplete="#{rich:component('prefPanel')}.show()"
  3. In the action handler on the server, be sure to load any entities needed by the ModalPanel into the extended PersistenceContext for the conversation:
    // Out-ject the User object that we're updating, vs. the one that came in ...
        @Out protected User updatingUser = null;
    
        // Just a handy reference to the preferences object we're updating ...
        @Out protected Preferences updatingPrefs = null;
    
        @Begin(flushMode = FlushModeType.MANUAL, join = true)
        public void beginEditPreferences() {
            // load the User entity to edit preferences for into the extended PC for this conversation
            updatingUser = getEntityManager().find(User.class, currentUser.getId());
            updatingPrefs = updatingUser.getPreferences();
            initDob();
        }
    

Processing the Form on the ModalPanel (and ending the Conversation)
So now you have a visible ModalPanel with a form displayed, within a Seam conversation. When the user submits the form, we need to validate the input and display any errors that occur. If no errors occur, then close the panel when the action completes. JSF syntax to process a form on ModalPanel.
  1. The action handler should end the conversation only if it completes successfully. So be careful with using @End with AJAX action handlers of type void. I prefer to use Conversation.instance().end() when dealing with AJAX actions as it makes it more explicit when the conversation ends.
  2. Be sure to re-render the form after submit so validation errors are displayed correctly.
  3. As mentioned previously, use a simple JavaScript function to determine if there are FacesMessages with severity ERROR or greater queued in the FacesContext; close the panel if there are no errors.
Queue and Traffic Flood Protection using the Global Default Queue
I've enabled the global queue for this application in web.xml:
<context-param>
    <param-name>org.richfaces.queue.global.enabled</param-name>
    <param-value>true</param-value>
  </context-param>

Project Setup
Building
You can download the project from here.
The project should build with minimal effort using Seam 2.2.0 with Java 6 from the command-line or Eclipse (I used v. 3.4.2). Once deployed to your JBoss 4.2.3 server, you can register or login as "vinwiki" with password "S00ner$1". However, you should also register your own account to see how the registration system works.
Database Setup
I've included a mysqldump file (vinwiki.sql) in the root directory which contains some wine related objects pulled from Freebase. To install this database in your MySQL 5.1.x database, do the following:
mysql> create database vinwiki;
mysql> grant all privileges on vinwiki.* to 'vinwiki'@'localhost' identified by 'vinwiki';
mysql> use vinwiki;
mysql> source vinwiki.sql;
Testing
The tests are configured to run against a MySQL database named vinwiki_test so that tests do not affect the development database. The JDBC connection parameters are specified in: bootstrap/deploy/hsqldb-ds.xml. From the MySQL command-line, do the following:
mysql> create database vinwiki_test;
mysql> grant all privileges on vinwiki_test.* to 'vinwiki'@'localhost' identified by 'vinwiki';

What's Next?
So by now you should have a good understanding of the basic structure of this application and be able to build it and run the unit tests. Please continue on to the second post which covers Hibernate Search and Lucene to help users find wines of interest.

8 comments:

  1. Wow! Thank you for sharing this incredibly valuable information. Great examples.

    ReplyDelete
  2. Looking forward to the rest of the series. Excellent work!

    ReplyDelete
  3. FYI: I downloaded the vinwiki.zip, ran the mysql stuff and tried to deploy on JBoss. I got a NullpointerException and it failed on deployment. I will digg into it further when I get a chance. But I think it would be better to create a GIT or SVN repository (or even google code) project so that we can submit bugfixes/enhancements etc so that it would benefit entire community.

    ReplyDelete
  4. Please post the NPE you received when trying to deploy.

    I definitely plan to add this to a site where people can check out the project and monitor changes (probably Google code). I will post this once I've completed the 3rd posting (~1 week from now). Do you have any other suggestions on sites where I could post this?

    ReplyDelete
  5. I've only tested with JBoss 4.2.3. It seems that something went terribly wrong in the deployment because the following line is producing the NPE:

    IndexReader reader = readerProvider.openReader(searchFactory.getDirectoryProviders(Wine.class)[0]);

    For now, try to use JBoss 4.2.3 ... I'm planning to port the app to JBoss 6.x (with the latest Seam, JSF, RF, and Hibernate) in a few weeks.

    ReplyDelete
  6. I can confirm that JBoss 4.2.3 worked fine

    ReplyDelete
  7. well done!! examples like this are very usefull, thank you very much

    ReplyDelete
  8. Hey, this tutorial is the best I have seen so far about integrating a lot of new technologies and APIs... Where can I download the vinwiki.zip??? Could you provide the github or source-code repository and we all could work on it and learn?

    thanks
    Marcello

    ReplyDelete