Some content

Sample App Walkthrough

We are going to walktrough a sample application, sampleBookmarks showing basic login/authentication, @Web[REST] for JSON APIs, SEO oriented pages with @WebModelHandler and the optional but very simple and customizable Snow Hibernate support (with HibernateDaoHelper and HSQLDB)

Note In this sample application, we are following some of our best practices on how to organize the various classes, methods name, and DAO pattern, but Snow is completely annotation driven and does not enforce any naming convention.

Clone, Build, Run

Clone it: (or download it from github)

$ git clone git://github.com/BriteSnow/sampleBookmarks.git

Build it:

$ cd sampleBookmarks  
sampleBookmarks/$ mvn clean package  

Run it:

sampleBookmarks/$ mvn jetty:run  

And open http://localhost:8080/

Optional: For eclipse users (assuming you have setup Eclipse to work with Maven).

sampleBookmarks/$ mvn eclipse:eclipse  

Application Config

Snow applications are configured via one or more Google Guice modules. At initialization time, Snow gets the list of application Guice modules from the snow.webApplicationModules property from the WEB-INF/snow.properties file.

webapp/WEB-INF/snow.properties

# The Application Guice Modules (1 or more, comma separated)
snow.webApplicationModules=com.example.samplebookmarks.AppConfig

Where AppConfig is a Google Guice module, with its simplest implementation as:

com.example.samplebookmarks.AppConfig

package com.example.samplebookmarks;

public class AppConfig extends AbstractModule {
    
    @Override
    protected void configure() {
        // some bindings
    }
}

NoteThe first Google Guice module defined in the snow.webApplicationModules property (here the only AppConfig ) specifies the base package from where Snow scans classes for Snow method annotations (e.g., @WebGet, @WebModelHandler, ...) to do the appropriate "Web bindings." So, make sure that the first Guice module is at the root package of your application.

Authentication & Login

Implementing login and authentication in Snow is dead simple. The only things needed are to bind a AuthRequest implementation, which will attempt to authenticate any incoming request, and implement the @WebPost for the for the login and logoff REST api (in this application, we use "/api/user-login" for login, and "/api/user-logoff") to perform the login and logoff.

So, the request authentication and login/logoff request flow will look like:

The AuthRequest.authRequest returns null until the user does a POST at the "/api/user-login" with the appropriate credential.

To implement AuthRequest we just need to "Google Guice" bind AuthRequest.class interface to an application implementation, such as:

com.example.samplebookmarks.AppConfig

package com.example.samplebookmarks;

public class AppConfig extends AbstractModule {
  protected void configure() {
      bind(AuthRequest.class).to(AppAuthRequest.class);
  }
}

This AuthRequest interface just have one method, AuthRequest.authRequest which takes a RequestContext object (a Snow convenient wrapper of HttpServletRequest and HttpServletResponse), and need to return a AuthToken with a given user when the user has been logged-in, or just return null (meaning, no user authenticated for this request).

com.example.samplebookmarks.web.AppAuthRequest

public class AppAuthRequest implements AuthRequest<User> {

    @Override
    public AuthToken<User> authRequest(RequestContext rc) {
        // Note for this sample app, we store the user in the session, 
        // but for production application, use stateless authentication mechanism for 
        // better sclability.
        
        // RequestContext is a convenient Snow wrapper on top of HttpServletRequest, 
        //  and HttpServletResponse
        // rc.getReq() return the HttpServletRequest 
        User user = (User) rc.getReq().getSession().getAttribute("user");
        
        if (user != null){
            AuthToken authToken = new AuthToken();
            authToken.setUser(user);
            return authToken;
        }else{
            return null;
        }
    }

}

Note Below the generic type User belong to the application, and not to Snow. This allows developer to have any User model they think is the most appropriate to their application. See AuthRequest documentation for more information.

Doing a login API is as simple as annotating the login and logoff with @WebPost as:

com.example.samplebookmarks.web.LoginWebHandlers

@Singleton
public class LoginWebHandlers {

    @Inject
    private UserDao userDao;
    
    /**
     * Note: Here the RequestContext will be injected by Snow 
     *          
     */
    @WebPost("/api/user-login")
    public WebResponse login(@WebParam("userName")String userName, 
                             @WebParam("pwd")String pwd, 
                             RequestContext rc){
        User user = userDao.getUserByUserName(userName);
        
        if (user != null && pwd != null && pwd.equals(user.getPassword())){
            rc.getReq().getSession().setAttribute("user", user);
            return WebResponse.success();
        }else{
            return WebResponse.fail("Wrong username or password.");
        }

    }

    /**
     * Note: Here we show how we can inject HttpServletRequest directly
     * @param req
     */
    @WebPost("/api/user-logoff")
    public void logoff(HttpServletRequest req){
        req.getSession().removeAttribute("user");
    }
    
}

First, we annotate LoginWebHandlers class with @Singleton as any "WebClass" (i.e. classes containing Snow @Web[REST] or any other @Web...Handler annotation) should be of scope singleton (See JSR 330 annotation for more information about javax.inject annotations).

Second, since this class will be managed by Google Guice, we can use the @Inject for injecting the instance of UserDao into private UserDao userDao.

Finally, we need to implement the two @Web[REST] methods, @WebPost("/api/user-login") and @WebPost("/api/user-logoff") that will perform the login and logof actions.

The parameters of any @Web[REST] methods are completely dynamic, and Snow will inject the appropriate value given the HTTP request context and the parameter annotation and type.

@WebParam is a built-in parameter Snow annotation that tell Snow to take corresponding the HTTP parameter value and converts it to the java parameter type (primitive and enum) value.

Also, the above code shows that parameters, @Web[REST] methods can also have parameters of type RequestContext or HttpServletRequest and it will be approprietally injected by Snow.

A @Web[REST] method can return a Object that would be serialized to json by the default JsonRenderer (which can also be customized). In this sample application, we have a universal response format with a custom WebResponse wrapper.

Note A @Web[REST] method can also return a String, and in this case, it will be assumed to be the JSON string to be returned.

@Web[REST] item methods

Now, that the implmentation, code is done, we can do the REST API for the create and delete of the Item entities.

So, to get the following REST API:

  • Item Create: HTTP POST: /api/user-create-item?title=[title]&url=[url]&note=[note]
  • Item Delete: HTTP DELETE: /api/user-item-[id]
  • Item List: HTTP GET: /api/user-items (not used in the UI, but just as a sample API (http://localhost:8080/api/user-items)

The flow would be:

We would code something like this:

com.example.samplebookmarks.web.ItemWebHandlers

  
@Singleton
public class ItemWebHandlers {

    @Inject
    public ItemDao itemDao;

    @WebPost("/api/user-create-item")
    public WebResponse apiUserCreateItem(@WebUser User user, 
                                         @WebParam("title") String title,
                                         @WebParam("url") String url, 
                                         @WebParam("note") String note) {
        if (user != null) {
            try {
                Item item = new Item(title, url, note);
                item.setUser_id(user.getId());
                item = itemDao.save(item);
                return WebResponse.success(item);
            } catch (Throwable t) {
                return WebResponse.fail(t);
            }
        }
        return WebResponse.fail("Not logged in, no create.");
    }

    @WebDelete("/api/user-item-{id}")
    public WebResponse apiUserDeleteItem(@WebUser User user, 
                                         @PathVar("id") Long id) {
        if (user != null) {
            try {
                itemDao.delete(id);
                return WebResponse.success(id);
            } catch (Throwable t) {
                return WebResponse.fail(t);
            }
        }
        return WebResponse.fail("Not logged in, no delete.");
    }

    @WebGet("/api/user-items")
    public WebResponse apiUserItems(@WebUser User user) {
        if (user != null) {
            try {
                List items = itemDao.getItemsForUser(user.getId());
                return WebResponse.success(items);
            } catch (Throwable t) {
                return WebResponse.fail(t);
            }
        }
        return WebResponse.fail("Not logged in, no item list.");
    }

}

As with the login/logoff API, the @WebPost, @WebDelete, and @WebGet annotations tell Snow to bind these java methods to their corresponding HTTP methods and paths.

The class (here ItemWebHandlers) will be instantiated by Guice and so can have all the Google Guice goodies like @Inject, and AOP.

Here we have two new parameter Snow annotations:

  • @WebUser is a built-in Snow param annotation that inject the User authenticated by the eventually bound AuthRequest. If no user authenticated for this request, @WebUser user == null
  • @PathVar is a built-in Snow annotation that takes the value from path from the {.} notation (here "id" given the path /api/user-item-{id}).

The Snow http-to-java parameter resolution flow is extremely simple and powerful to extend. See @WebParamResolver for more info.

Best Practice Here the class with the @Web[REST] annotation is in the .web java package, but Snow does not enforce any naming or code organization. The only requirement, is that your first AppConfig is at a package at the same level or above your Snow annotated Java classes (which is the case in this project)

Page model (e.g., /bookmarks)

Snow takes a cascading pattern for model building as well as view rendering. The page model is built by one or more Java methods (annotated with @WebModelHandler(startsWith="/...") and views are render with one or more freemarker templates.

For example, the /bookmarks page execution flow will look like this:

The Model building will be done in Java, with the following two @WebModelHandler. The "/" will be executed for all pages, and the "/bookmarks" will be executed for the URI "/bookmarks" or "/bookmarks/*".

com.example.samplebookmarks.web.MultiPageWebHandlers

@Singleton
public class MultiPageWebHandlers {

    @Inject
    private ItemDao itemDao;
    
    @WebModelHandler(startsWith="/")
    public void allPages(@WebModel Map m){
        m.put("version", "1.1.0");
    }
    
    /**
     * 
     * @param m (@WebModel) will be injected by Snow, and it is the WebModel for the page 
     * @param user (@WebUser) will be injected by Snow, and represents the  
     *                       Authenticate user. Null for un-authenticated request.
     */
    @WebModelHandler(startsWith="/bookmarks")
    public void bookmarksPage(@WebModel Map m, @WebUser User user){
        if (user != null){
            m.put("items", itemDao.getItemsForUser(user.getId()));
        }
    }
}
  

The views are implemented in freemarker templates, and for this sample application, we are using the Snow cascading templating support (i.e. _frame.ftl).

The cascading templating system of Snow is very simple. When a "_frame.ftl" is present in the path of a target page, it will be included first, the template can decide to include the target content (or the eventual nested _frame.ftl) by using the Snow built-in freemarker template directive, [@includeFrameContent /].

So, first, we have the /_frame.ftl which will get included for every pages.

/_frame.ftl

<!DOCTYPE html>
<html>
  <head>
     ...
  </head>

  <body>
    [#if _r.user??]
      <!-- Note: "includeFrameContent" is a Snow specific freemarker directive that allow to include the targeted template
        for this URL respecting the "_frame.ftl" hierarchy  -->
      [@includeFrameContent /]
    [#else]
      [@includeTemplate name="loginform.ftl"/] 
    [/#if]
    <footer>${version}</footer>
  </body>
</html>  

Here, in this template, we just have a [#if _r.user??] which test if a "user" has been authenticated for this request (_r being a built-in Snow freemarker object with some request properties) and if we have a user, we include the next _frame.ftl or target page, by using the [@includeFrameContent /].

Then, the bookmarks.ftl can just focus on the content:

/bookmarks.ftl

[@includeTemplate name="navbar.ftl" /]
<section class="content">

  .... addBookmark tags and script ....
  
  <h4>Bookmarks</h4>
  
  <ul id="itemList1" class="itemList">
    [#list items as item]
      <li data-entity-id="${item.id}">
        <a href="${item.url!'no url'}">${item.title!'no title'}</a>
        <p class="muted">${item.note}</p>
        <i class="icon-remove-sign do-delete"></i>
      </li>
    [/#list]
  </ul>  
  ....
</section>

Note 1 The [@includeTemplate name="..."/] is another Snow built-in freemarker directive that allows to include a freemarker template relative to the webapp folder.

Note 2 In this example application, we are using the simple jQuery AJAX Form plugin which turns any form tags into an AJAX call (which is one of the best practice we follow at BriteSnow). See bookmarks.ftl script tag to see a code sample.

Adding HibernateDaoHelper

While Hibernate is not required, Snow has built-in Hibernate support, which is very simple and customizable.

To enable Hibernate Snow support:

1) Add hibernate.* properties in webapp/WEB-INF/snow.properties

# The Application Guice Modules (1 or more, comma separated)
snow.webApplicationModules=com.example.samplebookmarks.AppConfig

# Hibernate Config
hibernate.connection.driver_class = org.hsqldb.jdbcDriver
hibernate.connection.url=jdbc:hsqldb:file:tmp/sampleBookmarksDB/hsqldb
hibernate.connection.username=testdb_user
hibernate.connection.password=welcome
hibernate.connection.autocommit=true
hibernate.dialect = org.hibernate.dialect.HSQLDialect
hibernate.hbm2ddl.auto = update

# optional Hibernate/c3p0 section properties ... (see source file) 

2) Guice provide a @EntityClasses Class[] as com.example.samplebookmarks.AppConfig

package com.example.samplebookmarks;

public class AppConfig extends AbstractModule {
    
    ....
    
    @Provides
    @Singleton
    @EntityClasses
    public Class[] provideEntityClasses() {
      // Here is a simple harcoded implementation for this method
      return new Class[]{com.example.samplebookmarks.entity.User.class,
                         com.example.samplebookmarks.entity.Item.class};
      // See the source file for auto-discovery implementation                   
    }
}

Once this is done, Snow automatically initialize Hibernate, and expose a very convenient HibernateDaoHelper instance that can be injected in any DAO or object that needs to make HQL calls. See the com.example.samplebookmarks.dao.BaseHibernateDao to see the use of HibernateDao.

Note Obviously, Hibernate is completely optional in Snow, and developers can use database and storage libraries of their choosing, and even provide their own support for Hibernate.