Wednesday, March 30, 2011

Part IV - Customization of App Engine Authentication for Roo-Generated GWT Projects

In This Post

We are going to make the Roo-generated ApplicationScaffold.html host page accessible only by admin users and create a second host page visible to all users.  While regular users will only see the new host page, admin users will be able to switch back and forth.  We are going to implement this functionality without creating a new GWT module and leave this topic to be discussed in my next post.


Before We Start

I have updated the previous post with a fix that needs to be applied to the Roo-generated code before proceeding to the next section.  For more information on this fix, please follow this link.

Before jumping into the customization of our Pizza Shop project, lets go through some prerequisite facts that we need to know.


Google App Engine (GAE) Login Functionality

The first screen we are presented with when we launch our Pizza Shop Web application consists of a mock GAE login form.  This mock login form allows us to test our application's integration with the GAE Users API and its login functionality without having to deploy our application.  There is no actual authentication taking place in this mock login screen.  Anything entered will be accepted.  The "Sign in as Administrator" checkbox allows us to test how our application handles users with administrative privileges.


GWT Wizards

The Wizards that GWT has made available for us will come in handy in this post as well as the next one.  In order to access these wizards, select the "New" submenu's "Other..." item from the project's context menu.



Scroll down to the "Google Web Toolkit" folder to view all GWT wizards available.


We will be working with "HTML Page" and "UiBinder" wizards in this post.


Servlet Filter for Authentication

Our Roo-generated scaffold application uses the ~.server.gae.GaeAuthFilter servlet filter to grant access only to those users who have logged in to the application.

public class GaeAuthFilter implements Filter {
    public void destroy() {
    }

    public void doFilter(ServletRequest servletRequest,
            ServletResponse servletResponse,
            FilterChain filterChain) throws IOException, ServletException {
        UserService userService = UserServiceFactory.getUserService();
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        if (!userService.isUserLoggedIn()) {
            String requestUrl = request.getHeader("requestUrl");
            if (requestUrl == null) {
                requestUrl = request.getRequestURI();
            }
            response.setHeader("login", userService.createLoginURL(requestUrl));
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        filterChain.doFilter(request, response);
    }

    public void init(FilterConfig config) {
    }
}

GaeAuthFilter rejects requests made by users who are not logged in by sending an error to the client, which in turn redirects the users to the login screen.

Filter declaration and mapping of GaeAuthFilter is done in the deployment descriptor of our project.

<filter>
    <filter-name>GaeAuthFilter</filter-name>
    <filter-class>com.blogspot.gwtsts.pizzashop.server.gae.GaeAuthFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>GaeAuthFilter</filter-name>
    <url-pattern>/gwtRequest/*</url-pattern>
</filter-mapping>

The filter-mapping element contains the "/gwtRequest/*" URL pattern, which covers all GWT requests at the root directory level.

You may be asking yourself why we need the security constraint configuration if authorization is handled by the GaeAuthFilter.

<security-constraint>
    <display-name>Redirect to the login page if needed before showing any html pages</display-name>
    <web-resource-collection>
        <web-resource-name>Login required</web-resource-name>
        <url-pattern>*.html</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>*</role-name>
    </auth-constraint>
</security-constraint>

The security constraint configuration will provide security for all non-GWT HTML files in our Web application.


URL Pattern Syntax

The following is valid syntax for URL patterns defined in the deployment descriptor.
  • Strings beginning with a / character and ending with a /* suffix are used for path mapping
  • Strings beginning with a *. prefix are used in extension mapping
  • The / character alone indicates a default mapping for the application
The container attempts to find an exact match for all strings that don't match the above criteria.


URL Pattern Processing

The container will try find a match in the following order and stop searching when a match is found.
  • Exact match of the request path to a configured path
  • Longest configured path matching the request path
  • If the request path ends with an extension (ie. .html), then the container will search for a matching extension mapping
  • If a default mapping exists, then the container will use it
Our Roo-generated application uses all mapping methods except for exact path mapping.


Targeted Functionality

We are now ready to define how our Web application will behave after we have implemented our customizations.  We are targeting the following logical flow in our Web application:
  • A user logs in to the Web application
  • The user is presented with a new custom screen
  • If the user has GAE administrative privileges, then a link to the original scaffold screen is displayed.  This link will not be visible for regular users without admin privileges.
  • Admin users who follow this link will be taken to a new host page that displays the original scaffold screen.
  • The scaffold screen will contain a link to the entry screen.  Admin users will return to the new screen when this link is clicked.
In short, we will be using the Roo-generated scaffold interface as an administration console that is only available to users with GAE administrative privileges and creating a new screen for regular users.


Implementation

We will start our customization by creating a new GWT host page for our administration console.
  • Create admin/console.html host page using "HTML Page" GWT wizard.

After adding a viewport meta tag, editing the title, and adding a span tag with a message stating that the console is being loaded, we will end up with the following.

<!doctype html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>Administration Console</title>
    <script type="text/javascript" language="javascript" src="../applicationScaffold/applicationScaffold.nocache.js"></script>
  </head>
  <body>
    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
    <span id='loading'
       style="margin-left:10px;font-size:12px;padding:10px;font-family:Helvetica;background-color:#e5edf9;border:2px solid #96a2b5;">
      Loading Admin Console…
    </span>
  </body>
</html>

  •  ApplicationScaffold.html will be the host page accessible for all users, so lets edit the title in ApplicationScaffold.html to something more appropriate.
<title>Pizza Shop</title>
  • Now that we have two different host pages, the time has come to limit access to one of them.  We will start by creating a new filter class in the ~.server.gae package.
public class GaeAdminFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
            FilterChain chain) throws IOException, ServletException {
        UserService userService = UserServiceFactory.getUserService();
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        if (!userService.isUserLoggedIn() || !userService.isUserAdmin()) {
            String requestUrl = request.getHeader("requestUrl");
            if (requestUrl == null) {
                requestUrl = request.getRequestURI();
            }
            response.setHeader("login", userService.createLoginURL(requestUrl));
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }
}

GaeAdminFilter behaves exactly the same way as GaeAuthFilter, except that it requires users to have admin privileges in addition to being logged in.  It also sends a different error code in the case that these requirements are not met.
  • We have created GaeAdminFilter, but it won't be used unless we add it to our deployment descriptor.  So, lets insert the filter declaration and filter mapping for GaeAdminFilter into our web.xml file.
<filter>
    <filter-name>GaeAdminFilter</filter-name>
    <filter-class>com.blogspot.gwtsts.pizzashop.server.gae.GaeAdminFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>GaeAdminFilter</filter-name>
    <url-pattern>/admin/gwtRequest/*</url-pattern>
</filter-mapping>

Notice that the URL pattern for GaeAdminFilter is longer than that of GaeAuthFilter and thus will have higher priority as per URL pattern processing rules described above.
  • Another change that we have to make to the deployment descriptor is to add a requestFactory servlet mapping for the new admin directory as shown below.
<servlet-mapping>
    <servlet-name>requestFactory</servlet-name>
    <url-pattern>/admin/gwtRequest</url-pattern>
</servlet-mapping>

Without this servlet mapping configuration, we would receive "HTTP 404 - Not Found" error when attempting to make a GWT request from our console.html file.  Note that all mapping elements, including this servlet mapping element, have to be placed after the corresponding declaration, which is the requestFactory servlet declaration in this case.
  • To avoid a runtime exception, we need to handle the new SC_FORBIDDEN error code returned by GaeAdminFilter in the case of request denial.  This will be done by adding the following block to handle the new error code in createRequestCallback() method of ~.client.scaffold.gae.GaeAuthRequestTransport.
if (Response.SC_FORBIDDEN == statusCode) {
    String loginUrl = response.getHeader("login");
    if (loginUrl != null) {
        receiver.onTransportFailure(new ServerFailure(
            "Forbidden content", null, null, false /* not fatal */));
        eventBus.fireEvent(new GaeAuthenticationFailureEvent(loginUrl));
        return;
    }
}

At this point, our new HTML host page is only accessible by admin users but displays the same content as the original host page.  You should be able to run your application and verify that admin users can view both host pages, whereas normal users will be redirected to the login screen when attempting to access console.html.

The next task on our to-do list is to change the content that is displayed by each host page.  We want the new console.html to display the original scaffold application, which we intend to use as an administration console, and the ApplicationScaffold.html to display new content.
  • Lets start by creating a new ~.client.custom.ui.CustomView using UiBinder GWT wizard, which will create a CustomView.ui.xml and a CustomView.java file.  CustomView.ui.xml will be a copy of ScaffoldDesktopShell.ui.xml without the entity list and entity details elements and their corresponding style declarations.
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
    xmlns:g="urn:import:com.google.gwt.user.client.ui"
    xmlns:s="urn:import:com.blogspot.gwtsts.pizzashop.client.scaffold.ui">

  <ui:image field='gwtLogo' src="../../style/images/gwtLogo.png"/>
  <ui:image field='rooLogo' src="../../style/images/rooLogo.png"/>
  <ui:style>
  ...
  </ui:style>

  <g:DockLayoutPanel unit='EM'>
    <g:north size='6'>
      <g:HTMLPanel styleName='{style.centered}'>
        <div class='{style.banner}'>
          <div class='{style.error}' ui:field='error'></div>
          <span class='{style.title}'>
            <h2>Pizza Shop</h2>
          </span>
          <s:LoginWidget styleName='{style.login}' ui:field="loginWidget"/>
        </div>
      </g:HTMLPanel>
    </g:north>
    <g:south size='2'>
      <g:HTML>
        <div class='{style.logos}'>
          <span>Powered by:</span>
          <a href='http://code.google.com/webtoolkit/'>
            <div class='{style.gwtLogo}'></div>
          </a>
          <a href='http://www.springsource.org/roo/'>
            <div class='{style.rooLogo}'></div>
          </a>
        </div>
      </g:HTML>
    </g:south>
    <g:center>
      <g:HTML>
        <h1 style='text-align: center'>Welcome to the Pizza Shop!</h1>
      </g:HTML>
    </g:center>
  </g:DockLayoutPanel>
</ui:UiBinder>

We will need to add LoginWidget field and its accessor method to CustomView.java to give us the following.

public class CustomView extends Composite {

    private static CustomViewUiBinder uiBinder = GWT
            .create(CustomViewUiBinder.class);
 
    @UiField
    LoginWidget loginWidget;

    interface CustomViewUiBinder extends UiBinder<Widget, CustomView> {
    }

    public CustomView() {
        initWidget(uiBinder.createAndBindUi(this));
    }

    /**
     * @return the login widget
     */
    public LoginWidget getLoginWidget() {
        return loginWidget;
    }
}
  • We now have to return to our console.html host page to find a way to differentiate it from ApplicationScaffold.html.  The span tag for the "loading" message is a perfect candidate for this.  Setting the id attribute of the span tag to something that differs from the one in ApplicationScaffold.html will allow us to differentiate the two host pages.
<span id='loadingConsole' style="...">
    Loading Admin Console…
</span>
  • We will update ~client.scaffold.ScaffoldDesktopApp's run() method to check if the host page contains an element with id attribute equal to loading or loadingConsole.  This will allow us to determine which host page we are dealing with.  
public void run() {
    /* Add handlers, setup activities */
    init();

    LoginWidget loginWidget = null;
    Widget mainWidget = null;
  
    Element loading = Document.get().getElementById("loading");
    if (loading != null) {
        CustomView customView = new CustomView();
        loginWidget = customView.getLoginWidget();
        mainWidget = customView;
   
        loading.getParentElement().removeChild(loading);
    }

    Element loadingConsole = Document.get().getElementById("loadingConsole");
    if (loadingConsole != null) {
        loginWidget = shell.loginWidget;
        mainWidget = shell;
   
        loadingConsole.getParentElement().removeChild(loadingConsole);
    }

    // AppEngine user authentication
    new GaeLoginWidgetDriver(requestFactory).setWidget(loginWidget);

    /* And show the user the shell */
    RootLayoutPanel.get().add(mainWidget);
}

Notice that the LoginWidget initialization by GaeLoginWidgetDriver has moved from the init() method to the run() method as the instance of LoginWidget to be initialized depends on the host page being accessed.

At this point, our new HTML host pages are displaying different content.  The original scaffold view has become our administration console, accessible only by admin users through admin/console.html, and our new view is accessible by all registered users through ApplicationScaffold.html.

Now, lets link the two pages to each other by adding new Anchor widgets to both host pages.  We want a link to the administration console that is only visible to users with administrative privileges on CustomView and a link that will return admin users back to the CustomView on the administration console (ScaffoldDesktopShell).
  • We will start by creating a new ~.client.scaffold.ui.ReturnLinkWidget using the UiBinder GWT wizard.  ReturnLinkWidget will be simple and contain only one Anchor widget styled the same way as LoginWidget for consistency.
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
    xmlns:g="urn:import:com.google.gwt.user.client.ui">
  <ui:style>
    .link {
      /* make it look like text */
      color: inherit;
      text-decoration: inherit;
    }
  </ui:style>
  <g:HTMLPanel>
    <div>
      <g:Anchor ui:field="returnLink" addStyleNames="{style.link}">Pizza Shop</g:Anchor>
    </div>
  </g:HTMLPanel>
</ui:UiBinder>

Adding an Anchor field and a setReturnUrl() method to ReturnLinkWidget.java will give us the following.


public class ReturnLinkWidget extends Composite {
    private static ReturnLinkWidgetUiBinder uiBinder = GWT
        .create(ReturnLinkWidgetUiBinder.class);
 
    @UiField
    Anchor returnLink;

    interface ReturnLinkWidgetUiBinder extends
            UiBinder<Widget, ReturnLinkWidget> {
    }

    public ReturnLinkWidget() {
        initWidget(uiBinder.createAndBindUi(this));
    }
 
    public void setReturnUrl(String url) {
        returnLink.setHref(url);
    }
}
  • We will also need a driver for our new widget in the form of ~.client.console.ReturnLinkWidgetDriver which provides a setWidget() method.
public class ReturnLinkWidgetDriver {
    public void setWidget(final ReturnLinkWidget widget) {
        UrlBuilder urlBuilder = Window.Location.createUrlBuilder();
        urlBuilder.setPath("/ApplicationScaffold.html");
        widget.setReturnUrl(urlBuilder.buildString());
    }
}
  • Having accessed GWT's Window module, we are going to have to inherit it in our module definition (ApplicationScaffold.gwt.xml) file by inserting the following line.
<inherits name="com.google.gwt.user.Window"/>

  • Now that we have created ReturnLinkWidget, we can add it to our administration console view, ~.client.scaffold.ScaffoldDesktopShell.  Adding the ReturnLinkWidget and assigning it a custom style for placement will give us the following ScaffoldDesktopShell.ui.xml.
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
          xmlns:g='urn:import:com.google.gwt.user.client.ui'
          xmlns:s='urn:import:com.blogspot.gwtsts.pizzashop.client.scaffold.ui'>
  <ui:image field='gwtLogo' src="../style/images/gwtLogo.png"/>
  <ui:image field='rooLogo' src="../style/images/rooLogo.png"/>
  <ui:style>
    @def contentWidth 850px;

    ...

    .returnLink {
      position: absolute;
      left: 75%;
      right: 0%;
      top: 50%;
      text-align: center;
      color: #def;
    }
    ...
  </ui:style>

  <g:DockLayoutPanel unit='EM'>
    <g:north size='6'>
      <g:HTMLPanel styleName='{style.centered}'>
        <div class='{style.banner}'>
          ...
          <s:ReturnLinkWidget styleName='{style.returnLink}' ui:field="returnLinkWidget"/>
        </div>
      </g:HTMLPanel>
    </g:north>
    <g:south size='2'>
      ...
    </g:south>
    <g:center>
      ...
    </g:center>
  </g:DockLayoutPanel>
</ui:UiBinder>

Additionally, we will need to add the ReturnLinkWidget field and accessor method to ScaffoldDesktopShell.java.

public class ScaffoldDesktopShell extends Composite {

    ...

    @UiField
    ReturnLinkWidget returnLinkWidget;

    ...

    /**
     * @return the login widget
     */
    public ReturnLinkWidget getReturnLinkWidget() {
        return returnLinkWidget;
    }
    ...
}

  • One last modification we need to make before ReturnLinkWidget is fully operational is adding a call to ReturnLinkWidgetDriver's setWidget() method for the case when the admin/console.html host page is being accessed in ~.client.scaffold.ScaffoldDesktopApp's run() method.
public void run() {

    ...

    Element loadingConsole = Document.get().getElementById("loadingConsole");
    if (loadingConsole != null) {
        loginWidget = shell.loginWidget;
        mainWidget = shell;
        loadingConsole.getParentElement().removeChild(loadingConsole);
        new ReturnLinkWidgetDriver().setWidget(shell.returnLinkWidget);
    }

    ...
}

You should now be able to see this new link by accessing admin/console.html as an admin user.  Clicking the link should take you back to the CustomView.

We need to create one last widget that links admin users to the administration console before we can start selling pizzas online;  however, this task is not going to be as easy as ReturnLinkWidget, because we have the requirement of making the link visible only for admin users.  In order to accomplish this, we need to know if users have admin privileges on the client side, which means that we will be creating a new service request method.
  • Lets get started right away by updating ~.shared.gae.GaeUserServiceRequest with our service method declaration.
@Service(value = UserServiceWrapper.class, locator = UserServiceLocator.class)
public interface GaeUserServiceRequest extends RequestContext {
    ...
    public Request<Boolean> isUserAdmin();
}
  • Next step is to add a declaration to ~.server.gae.UserServiceWrapper interface.
public interface UserServiceWrapper {
    ...
    public Boolean isUserAdmin();
}
  • Final step is to add the implementation to ~.server.gae.UserServiceLocator.
public class UserServiceLocator implements ServiceLocator {
    public UserServiceWrapper getInstance(Class<?> clazz) {
        final UserService service = UserServiceFactory.getUserService();
        return new UserServiceWrapper() {
            ...
            @Override
            public Boolean isUserAdmin() {
                return new Boolean(service.isUserAdmin());
            }
        };
    }
}

We are now ready to create the ConsoleLinkWidget and add it to CustomView.
  • Lets use the UiBinder GWT wizard to create ~.client.custom.ui.ConsoleLinkWidgetConsoleLinkWidget.ui.xml will be very similar to  ReturnLinkWidget.ui.xml.
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
      xmlns:g="urn:import:com.google.gwt.user.client.ui">
  <ui:style>
    .link {
      /* make it look like text */
      color: inherit;
      text-decoration: inherit;
    }
  </ui:style>
  <g:HTMLPanel>
    <div>
      <g:Anchor ui:field="consoleLink" addStyleNames="{style.link}">Admin Console</g:Anchor>
    </div>
  </g:HTMLPanel>
</ui:UiBinder> 

  • An Anchor field and a setConsoleUrl() method will be added to ConsoleLinkWidget.java to produce the following.
public class ConsoleLinkWidget extends Composite {
    private static ConsoleLinkWidgetUiBinder uiBinder = GWT
                .create(ConsoleLinkWidgetUiBinder.class);
 
    @UiField
    Anchor consoleLink;

    interface ConsoleLinkWidgetUiBinder extends
        UiBinder<Widget, ConsoleLinkWidget> {
    }

    public ConsoleLinkWidget() {
        initWidget(uiBinder.createAndBindUi(this));
        this.setVisible(false);
    }
 
    public void setConsoleUrl(String url) {
        consoleLink.setHref(url);
    }
}

Notice that we have added a call to Widget's setVisible() method in the constructor.  This is because we want the Widget to be invisible by default.
  • As was the case for ReturnLinkWidget, next step is to create a widget driver that implements a setWidget() method.
package com.blogspot.gwtsts.pizzashop.client.console;
...
public class ConsoleLinkWidgetDriver {
    private final MakesGaeRequests requests;

    public ConsoleLinkWidgetDriver(MakesGaeRequests requests) {
        this.requests = requests;
    }
 
    public void setWidget(final ConsoleLinkWidget widget) {
        GaeUserServiceRequest request = requests.userServiceRequest();

        request.isUserAdmin().to(new Receiver<Boolean>() {
            public void onSuccess(Boolean response) {
                if (response.booleanValue() == true) {
                    UrlBuilder urlBuilder = Window.Location.createUrlBuilder();
                    urlBuilder.setPath("/admin/console.html");
                    widget.setConsoleUrl(urlBuilder.buildString());
                    widget.setVisible(true);
                }
                else {
                    widget.setVisible(false);
                }
            }
        });
        request.fire();
    }
}
  • Time has come to add ConsoleLinkWidget to the new ~.client.custom.ui.CustomView.  Adding the ConsoleLinkWidget and assigning it a custom style for placement will give us the following CustomView.ui.xml.
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
      xmlns:g="urn:import:com.google.gwt.user.client.ui"
      xmlns:s="urn:import:com.blogspot.gwtsts.pizzashop.client.scaffold.ui"
      xmlns:t="urn:import:com.blogspot.gwtsts.pizzashop.client.custom.ui">

  <ui:image field='gwtLogo' src="../../style/images/gwtLogo.png"/>
  <ui:image field='rooLogo' src="../../style/images/rooLogo.png"/>
  <ui:style>
    @def contentWidth 850px;

    ...

    .console {
      position: absolute;
      left: 75%;
      right: 0%;
      top: 50%;
      text-align: center;
      color: #def;
    }

    ...
  </ui:style>

  <g:DockLayoutPanel unit='EM'>
    <g:north size='6'>
      <g:HTMLPanel styleName='{style.centered}'>
        <div class='{style.banner}'>
          ...
          <t:ConsoleLinkWidget styleName='{style.console}' ui:field="consoleLinkWidget"/>
        </div>
      </g:HTMLPanel>
    </g:north>
    <g:south size='2'>
      ...
    </g:south>
    <g:center>
      ...
    </g:center>
  </g:DockLayoutPanel>
</ui:UiBinder>

Additionally, we will need to add the ConsoleLinkWidget field and accessor method to CustomView.java.

public class CustomView extends Composite {
 
    ...

    @UiField
    ConsoleLinkWidget consoleLinkWidget;

    ...

    /**
     * @return the login widget
     */
    public ConsoleLinkWidget getConsoleLinkWidget() {
        return consoleLinkWidget;
    }
}
  • Finally, lets add the code that initializes the ConsoleLinkWidget to ~.client.scaffold.ScaffoldDesktopApp's run() method.
public void run() {
    ...
  
    Element loading = Document.get().getElementById("loading");
    if (loading != null) {
        CustomView customView = new CustomView();
        loginWidget = customView.getLoginWidget();
        mainWidget = customView;
   
        loading.getParentElement().removeChild(loading);
   
        new ConsoleLinkWidgetDriver(requestFactory).setWidget(customView.getConsoleLinkWidget());
    }

    ...
}

We have achieved all our objectives at this point.  Only admin users are able to see a link that takes them to the admin console at this point.  They are able to follow this link to the admin console and follow the return link back to the main view.


Source Code

I have zipped up and uploaded the source directory for the Pizza Shop project.  The zip file includes all changes made in this post and can be downloaded via this link.


Uploaded To App Engine

I have also deployed the current state of the project to App Engine and it can be accessed via http://1.pizzashopexample.appspot.com.  IE9 users might have problems launching the application.

Deploying to App Engine is very easy.    Sign up to App Engine, register an application, and enter application ID in appengine-web.xml prior to deploying.  Then select "Google > Deploy to App Engine" from project's context menu, enter Google username and password, and you are done!


In The Next Post

We will be creating a new GWT module and linking two modules to each other.



IntroPart IPart IIPart IIIPart IVPart V



Tuesday, March 22, 2011

Part III - Exploring the Roo-Generated GWT Application

In This Post

We are going to take a look at what Roo has generated for us.  The generated project's structure and organization will be our first focal point.  Further into the post, we will delve into the architecture of our GWT application and discover its inner workings.  I will sporadically place links to further reading material that provide useful information on the given subject.


Project Organization

The STS Project Explorer gives us a tidied-up hierarchical view of all resources in our project.  Each resource has a context menu that allows a range of operations depending on the type of resource.


The Project Explorer provides easy access to the most important resources in our project.  Most of the time, we will be working with the following resources:
  • The Deployment Descriptor listing provides links to elements within our web.xml file for our Pizza Shop project.  The web.xml file is located in the %PROJECTROOT%\src\main\webapp\WEB-INF directory.
  • The Java Resources hierarchy contains all Java source code, properties files, GWT module and persistence configuration, and some image files, which GWT will bundle up into a single image to enhance the performance of our Web application.
  • Web Resources hierarchy contains HTML host pages and Google App Engine (GAE) configuration.


The Configuration

I will not go into the deployment descriptor as Google has prepared detailed documentation on the subject, including information on elements that are supported by GAE and a list of elements that are not.  It is a highly-recommended read for anyone who wishes to find out more on the subject.

The Roo-generated  appengine-web.xml is our GAE configuration file and defines the name of our GAE application, its version, property file location for GAE logging, and a switch to enable duplicate EntityManagerFactory creation as shown below.  It is located inside the %PROJECTROOT%\src\main\webapp\WEB-INF directory.

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
    <application>pizzashopexample</application>
    <version>1</version>
    <sessions-enabled>true</sessions-enabled>
    <!-- Configure java.util.logging -->
    <system-properties>
        <property name="java.util.logging.config.file"
          value="WEB-INF/logging.properties"/>
        <property name="appengine.orm.disable.duplicate.emf.exception" 
          value="false"/>
    </system-properties>
</appengine-web-app>

The logging.properties file allows the configuration of logging levels for the various loggers present in our application.  Once server-side logging has been enabled and application deployed, application logs can be viewed from GAE application management console.

The Roo-generated module definition file is located in the %PROJECTROOT%\src\main\java\%ROOTPACKAGE% directory.


Our module definition file is named ApplicationScaffold.gwt.xml and the root package for our Pizza Shop project is com.blogspot.gwtsts.pizzashop.  Inside the module definition file, we can see that our module has been given the name applicationScaffold.

<module rename-to="applicationScaffold">

Upon further examination of ApplicationScaffold.gwt.xml we can see that module definition files allow us to:
  • Declare other modules that we wish to inherit
    <inherits name='com.google.gwt.activity.Activity'/>
    <inherits name='com.google.gwt.place.Place'/>
    <inherits name="com.google.gwt.user.User"/>
    <inherits name="com.google.gwt.user.theme.standard.Standard"/>
    <inherits name='com.google.gwt.requestfactory.RequestFactory'/>
    <inherits name="com.google.gwt.user.cellview.CellView"/>
    <inherits name='com.google.gwt.logging.Logging'/>
    <inherits name="com.google.gwt.inject.Inject"/>
    <inherits name="com.google.gwt.text.Text"/>

  • Add packages to the source or public paths of our module
    <source path='client'/>
    <source path='shared'/>

    <public path="public"/>

  • Define propeties along with a set of allowed values
    <define-property name="mobile.user.agent" values="mobilesafari, none"/>

  • Set values for properties defined in our module or in inherited modules
    <set-property name="gwt.logging.enabled" value="TRUE"/>
    <set-property name="gwt.logging.logLevel" value="INFO"/>
    <set-property name="gwt.logging.consoleHandler" value="ENABLED"/>
    <set-property name="gwt.logging.developmentModeHandler" value="ENABLED"/>
    <set-property name="gwt.logging.firebugHandler" value="ENABLED"/>
    <set-property name="gwt.logging.hasWidgetsHandler" value="DISABLED"/>
    <set-property name="gwt.logging.popupHandler" value="DISABLED"/>
    <set-property name="gwt.logging.systemHandler" value="ENABLED"/>
    <set-property name="gwt.logging.simpleRemoteHandler" value="DISABLED"/>

  • Define deferred binding rules, including property providers and class generators
     <replace-with class="com.blogspot.gwtsts.pizzashop.client.scaffold.ioc.MobileInjectorWrapper">
       <when-type-is class="com.blogspot.gwtsts.pizzashop.client.scaffold.ioc.DesktopInjectorWrapper"/>
       <all>
         <when-property-is name="mobile.user.agent" value="mobilesafari"/>
       </all>
     </replace-with> 

  • Define the entry point class for our module.  More than one entry-point declaration can be made for each module, which will result in successive calls to the onModuleLoad() implementation for each EntryPoint class (including EntryPoints inherited from other modules).
    <entry-point class="com.blogspot.gwtsts.pizzashop.client.scaffold.Scaffold"/>

Also notice that the value of mobile.user.agent property is automatically read from the user's browser, which will allow us to display a customized view for mobile devices when such a device is used to access our application.

Now that we have covered most important points on the configuration side, lets look at the source code.


Model View Presenter Pattern

Our Roo-generated Web application uses the Model View Presenter (MVP) pattern, which separates functionality into the model, view, and presenter components.  The model is the data to be manipulated, the view takes care of interaction with the browser, and the presenter includes the rendering logic that formats data for display as well as the UI event handling that modifies application state in response to events generated by the view.


Advantages to using the MVP pattern include increased maintainability of medium to large Web applications and simplified and more efficient testing.  In a GWT application, the MVP pattern decouples UI widgets and presentation logic, allowing minimized use of the costly GWTTestCase, which relies on the presence of a browser. 


Startup

The onModuleLoad() method of our EntryPoint class is called when users launch the Pizza Shop Web application.  As previously mentioned, our EntryPoint class is declared in our module definition file (ApplicationScaffold.gwt.xml).  The last line of our module definition file states that our EntryPoint class is com.blogspot.gwtsts.pizzashop.client.scaffold.Scaffold.

public class Scaffold implements EntryPoint {
    final private InjectorWrapper injectorWrapper = GWT.create(DesktopInjectorWrapper.class);

    public void onModuleLoad() {
        /* Get and run platform specific app */
        injectorWrapper.getInjector().getScaffoldApp().run();
    }
}

The Scaffold class creates an instance of DesktopInjectorWrapper by default.  This class will be replaced by MobileInjectorWrapper if user's browser type is mobilesafari as configured in our module definition file.  By default, we end up calling ScaffoldDesktopApp's run() method.

public void run() {
    /* Add handlers, setup activities */
    init();

    /* Hide the loading message */
    Element loading = Document.get().getElementById("loading");
    loading.getParentElement().removeChild(loading);

    /* And show the user the shell */
    RootLayoutPanel.get().add(shell);
}

All event handlers are registered to the EventBus with the call to ScaffoldDesktopApp's init() method, lines 62 and 63 remove the "loading" message from display, and line 66 attaches our view to the root panel in the HTML document.

Lets take a more detailed look at the init() method.

private void init() {
    GWT.setUncaughtExceptionHandler(new GWT.UncaughtExceptionHandler() {
        public void onUncaughtException(Throwable e) {
            Window.alert("Error: " + e.getMessage());
            log.log(Level.SEVERE, e.getMessage(), e);
        }
    });

    if (LogConfiguration.loggingIsEnabled()) {
        // Add remote logging handler
        RequestFactoryLogHandler.LoggingRequestProvider provider = new RequestFactoryLogHandler.LoggingRequestProvider() {
            public LoggingRequest getLoggingRequest() {
                return requestFactory.loggingRequest();
            }
        };
        Logger.getLogger("").addHandler(
            new RequestFactoryLogHandler(provider, Level.WARNING,
            new ArrayList<string>()));
    }

    RequestEvent.register(eventBus, new RequestEvent.Handler() {
        // Only show loading status if a request isn't serviced in 250ms.
        private static final int LOADING_TIMEOUT = 250;

        public void onRequestEvent(RequestEvent requestEvent) {
            if (requestEvent.getState() == RequestEvent.State.SENT) {
                shell.getMole().showDelayed(LOADING_TIMEOUT);
            } else {
                shell.getMole().hide();
            }
        }
    });

    // AppEngine user authentication
    new GaeLoginWidgetDriver(requestFactory).setWidget(shell.loginWidget);
    new ReloadOnAuthenticationFailure().register(eventBus);

    CachingActivityMapper cached = new CachingActivityMapper(applicationMasterActivities);
    ProxyPlaceToListPlace proxyPlaceToListPlace = new ProxyPlaceToListPlace();
    ActivityMapper masterActivityMap = new FilteredActivityMapper(proxyPlaceToListPlace, cached);
    final ActivityManager masterActivityManager = new ActivityManager(masterActivityMap, eventBus);
    masterActivityManager.setDisplay(shell.getMasterPanel());

    ProxyListPlacePicker proxyListPlacePicker = new ProxyListPlacePicker(placeController, proxyPlaceToListPlace);
    HasConstrainedValue<proxylistplace> listPlacePickerView = shell.getPlacesBox();
    listPlacePickerView.setAcceptableValues(getTopPlaces());
    proxyListPlacePicker.register(eventBus, listPlacePickerView);

    final ActivityManager detailsActivityManager = new ActivityManager(applicationDetailsActivities, eventBus);

    detailsActivityManager.setDisplay(shell.getDetailsPanel());

    /* Browser history integration */
    ScaffoldPlaceHistoryMapper mapper = GWT.create(ScaffoldPlaceHistoryMapper.class);
    mapper.setFactory(placeHistoryFactory);
    PlaceHistoryHandler placeHistoryHandler = new PlaceHistoryHandler(mapper);
    if (getTopPlaces().iterator().hasNext()) {
        ProxyListPlace defaultPlace = getTopPlaces().iterator().next();
        placeHistoryHandler.register(placeController, eventBus, defaultPlace);
        placeHistoryHandler.handleCurrentHistory();
    }
}

UPDATE: Line 108 has to be added manually until a fix for ROO-2269 has been provided.

Lines 75-80 set up the mechanism to display any uncaught exceptions in a dialog box and add them to client-side logs.  The block of code that adds a remote logging handler in lines 82-92 will only get translated into JavaScript if logging has been enabled in our module definition file.  You can read more about client-side logging with GWT at this link.  Lines 94-105 deal with the "loading" message displayed at startup and line 109 arranges our Web application to be reloaded in case of an authentication failure.

The section from line 111 to line 134 makes up the most important part of our initialization.  This is where the our Presenter is set up.  Notice that we are creating two display regions; the master panel that displays a list of our domain object types and the details panel that displays a list of domain objects of the selected type.  I will comment more about this block of code in the Activity Pattern section.

The call to handleCurrentHistory() method of the PlaceHistoryHandler on line 133 triggers a PlaceChangeEvent that causes the default Activity to be start()ed.

After completion of the init() method, our Web application is running and ready to respond to user events.


UiBinder

The UiBinder framework allows us to build our applications as HTML pages with GWT widgets sprinkled throughout them.  It is not a renderer, which means that it contains no logic such as loops, conditionals, or if statements in its markup.   UiBinder allows us to lay out our widgets, but it is still up to the widgets themselves to convert rows of data into rows of HTML.  Read more here.


View

The ~.client.scaffold.ScaffoldDesktopShell makes up the view of the Roo-generated scaffold application.  It is made up of ScaffoldDesktopShell.ui.xml and ScaffoldDesktopShell.java bound together by the GWT UiBinder.  The following is the contents of the HTML-like ScaffoldDesktopShell.ui.xml file.

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
      xmlns:g='urn:import:com.google.gwt.user.client.ui'
      xmlns:s='urn:import:com.blogspot.gwtsts.pizzashop.client.scaffold.ui'>

  <ui:image field='gwtLogo' src="../style/images/gwtLogo.png"/>
  <ui:image field='rooLogo' src="../style/images/rooLogo.png"/>
  <ui:style>
    ...
  </ui:style>

  <g:DockLayoutPanel unit='EM'>
    <g:north size='6'>
      <g:HTMLPanel styleName='{style.centered}'>
        <div class='{style.banner}'>
          <div class='{style.error}' ui:field='error'></div>
          <span class='{style.title}'>
            <h2>Data Browser</h2>
          </span>
          <s:LoginWidget styleName='{style.login}' ui:field="loginWidget"/>
        </div>
      </g:HTMLPanel>
    </g:north>
    <g:south size='2'>
      <g:HTML>
        <div class='{style.logos}'>
          <span>Powered by:</span>
          <a href='http://code.google.com/webtoolkit/'>
            <div class='{style.gwtLogo}'></div>
          </a>
          <a href='http://www.springsource.org/roo/'>
            <div class='{style.rooLogo}'></div>
          </a>
        </div>
      </g:HTML>
    </g:south>
    <g:center>
      <g:FlowPanel styleName='{style.content} {style.centered}'>
        <g:SimplePanel styleName='{style.entities}'>
          <g:ValuePicker styleName="{style.entitiesList}" width='100%' pageSize='100' ui:field='placesBox'/>
        </g:SimplePanel>
        <g:FlowPanel>
          <g:NotificationMole animationDuration='0' message='loading...' ui:field='mole'></g:NotificationMole>
          <g:SimplePanel styleName="{style.entityDetails}" ui:field='master'></g:SimplePanel>
        </g:FlowPanel>
        <g:SimplePanel styleName="{style.entityDetails}" ui:field='details'></g:SimplePanel>
      </g:FlowPanel>
    </g:center>
  </g:DockLayoutPanel>
</ui:UiBinder>

UiBinder allows us to import Widget s defined in other packages in our class path.  Each imported package is assigned a letter, which is placed in front of Widget s used in the view to declare the package that the Widget resides in.  For example, the UiBinder searches the com.google.gwt.user.client.ui package when it encounters a <g:DockLayoutPanel> tag.

The <g:north> element contains the title bar of our application including a LoginWidget that displays the username and a logout link.  The <g:south> element contains logos for GWT and Roo.  The <g:center> element contains a panel where our Entity s are listed and a panel where details of the selected Entity are displayed.

Notice that we can use regular HTML tags in UiBinder files as long as they are placed inside a <g:HTML> tag.


Editor Framework

You might be wondering how the generated application reads and writes entity objects from and to the view.  It is all hidden away inside the GWT Editor Framework, which provides the functionality implicitly, using the marker interface pattern.


Security

The login screen that we see after launching our application is a mock App Engine login screen used to simulate GAE login functionality in development mode.  There is no view associated with this screen within our project.

I will be discussing application security and security configuration in detail in my next post.


The RequestFactory

The RequestFactory and JPA provide an easy way to build data-oriented CRUD applications and together make up the data-access layer of our Roo-generated application.

The following diagram depicts how the RequestFactory works with other components of our application to provide data synchronization.  This diagram is a slightly-modified version of the one used in the Architecting GWT Apps presentation at Google I/O 2010.


The program flow for creating a new PizzaOrder in our application would be as follows:
  • RequestContext's create() method is used to create an instance of PizzaOrderProxy
  • A new instance of PizzaOrderRequest is created
  • PizzaOrderProxy interface's accessor methods are used to modify the record
  • The persist() method is called on PizzaOrderRequest using() our PizzaOrderProxy instance
  • PizzaOrderRequest is fire()d off with a callback object that handles the case of success
  • Callback object is notified of success
  • Display is updated
Built to complement the service-oriented GWT RPC and not replace it, the data-oritented RequestFactory is GWT's new mechanism that provides more efficient client/server data transfer.  It allows us to define data-centric Entity classes on the server side and to define business logic with EntityProxy on the client side.


Entity

Entity s are server-side Data Access Objects (DAO) in our RequestFactory mechanism and should be placed in a package that will not be converted to JavaScript by GWT.  In our application, ~.server is such a package since we have not declared it as a source path in our module definition file.

In compliance with our directive,  Roo has placed all our Entity classes inside the ~.server.domain package.  The following is our Roo-generated Base class:

@RooJavaBean
@RooToString
@RooEntity
public class Base {
    @NotNull
    @Size(min = 2)
    private String name;
}

Notice that our Base Entity class contains only field declarations and annotations.  @NotNull annotation declares that this field cannot be null and @Size (min = 2) declares that the value of this field must contain 2 characters or more.  Accessor methods for our Base Entity are located in Base_Roo_JavaBean.aj AspectJ file, entity methods in Base_Roo_Entity.aj, and toString() method in Base_Roo_ToString.aj.

AspectJ files are maintained by Spring Roo and will be updated automatically in response to any changes we make to the Entity class.


EntityProxy

Objects that implement the EntityProxy interface are Data Transfer Objects (DTO) and client-side representations of corresponding Entity objects.  The exclusive use of EntityProxy interface on the client side relieves us from the requirement of writing GWT-compatible code in our Entity implementations.

The following is our BaseProxy interface:

package com.blogspot.gwtsts.pizzashop.client.managed.request;

import ...

@RooGwtMirroredFrom("com.blogspot.gwtsts.pizzashop.server.domain.Base")
@ProxyForName("com.blogspot.gwtsts.pizzashop.server.domain.Base")
public interface BaseProxy extends EntityProxy {
    abstract Long getId();
    abstract void setId(Long id);
    abstract Integer getVersion();
    abstract void setVersion(Integer version);
    abstract String getName();
    abstract void setName(String name);
}

The BaseProxy interface, along with others that extend EntityProxy interface, enables RequestFactory to determine fields that have changed and send only these changes to the server.  This functionality allows for more efficient client-server communication in GWT applications that use the RequestFactory.  Implementations of BaseProxy interface methods have been generated in the previously-mentioned Base_Roo_JavaBean.aj and Base_Roo_Entity.aj AspectJ files.


Activity Pattern

In the application that Roo has generated for us, the Activity is the implementation of the Presenter component and IsWidget is the implementation of the View component of our MVP pattern.  The following diagram, also taken from Ray Ryan's Architecting GWT Apps presentation, shows the life cycle of an Activity.


Notice how the ActivityManager responds to PlaceChangeEvents by swapping Activity s and as the next Activity is start()ed it communicates with the server to load the data necessary to initialize its view.


Activity

An Activity is completely isolated from the view and contains no widgets or UI code.  This isolation greatly simplifies the testing of logic contained within an Activity.  All that is needed to test an Activity is to tie it up to a mock ActivityManager and a mock RequestFactory.  An Activity is started with a call to its start() method, which takes a display region (AcceptsOneWidget) and an EventBus as arguments.  The initialization performed by the start() method can be run asynchronously.  In this case, the display region is passed the View object in the form of an object that implements IsWidget when the initialization is complete.


Place

A Place is a bookmarkable state for a Web application.  An Activity needs a corresponding Place in order to be accessible via a URL.  A Place extends com.google.gwt.app.place.Place and has an associated PlaceTokenizer that serializes the Place's state to a URL token.  By default, the URL consists of the Place's simple class name (ie. ProxyPlace) followed by a colon (:) and the string returned by the associated PlaceTokenizer's getToken() method.  The PlaceTokenizer can be declared as a static class inside the corresponding Place, but it is not mandatory to have a PlaceTokenizer for each Place.  Many Places within a Web application may not save their state to the URL and could just extend a BasicPlace, which comes with a PlaceTokenizer declaration that returns a null token.

As many Place subclasses as needed can be defined in a Web application, which will be instantiated anew each time users navigate within an application.  The Place class doesn't define any methods, but subclasses are expected to correctly implement Object.equals(Object) and Object.hashCode().  Subclasses of Place can also be used to carry data from one Activity to the next.

See ProxyPlace class inside the com.blogspot.gwtsts.pizzashop.client.scaffold.place package for an example of a Place in Roo-generated scaffold application.


PlaceController

The PlaceController manages the current Place and navigation between Places in a Web application and makes the back-button and bookmarks work as users would expect.

Since we are managing an application's state with the PlaceController, there should not be more than one PlaceController instance per application.   

It is the PlaceController 's goTo() method that fires PlaceChangeEvents on the EventBus, which in turn notifies registered components about these events.


ActivityManager

Each display region has its own ActivityManager, which responds to User action by stopping one Activity and starting another.  To put it another way, the ActivityManager swaps Activity s in its display region in response to PlaceChangeEvents.  As the next Activity is initialized with its start() method, the previous Activity is still displayed and accessible by the User.  Developers have the choice, at this point, of disabling the view, displayed by the stopped Activity, or to allow it to trigger navigation.

The following is application flow from the ActivityManager perspective:
  1. User navigates from one Place to another
  2. mayStop() method of the current Activity is called
  3. mayStop() asks for confirmation from the User before navigating via PlaceChangeRequestEvent
  4. If the confirmation is obtained, then a PlaceChangeEvent is fired and the current Activity is stopped with a call to its onStop() method
  5. The ActivityMapper determines the next Activity depending on the current place and passes it to the ActivityManager
  6. The next Activity's start() method is invoked by the ActivityManager that is responsible for the current display region (AcceptsOneWidget)
  7. When the next Activity is fully initialized, it asks to be displayed

Since we have two display regions in our application, two ActivityManagers have been created for us; the masterActivityManager and detailsActivityManager (see lines 114 and 122 shown in the Startup section).  Each ActivityManager is initialized with a display region to manage at module startup.

Note that the initialization of an Activity can be asynchronous, which would enable the User to change its mind before the Activity is fully initialized and navigate to another Place.  Instead of onStop() method of the current Activity, the onCancel() method would be called in this case, which doesn't require a prior call to mayStop() as the Activity isn't fully started.

Another issue to note is that there can be several Activity s running concurrently in different display regions.  Because of this, it is possible that multiple mayStop() requests are launched at the same time.  Not to worry though, as these won't interfere with each other.


ActivityMapper

ActivityMappers are associated with a display region and direct the ActivityManager to the appropriate Activity for a given Place.

For an example of an ActivityMapper generated for our Pizza Shop application, see com.blogspot.gwtsts.pizzashop.client.managed.activity.BaseActivitiesMapper.


ActivityWrapper

ActivityWrappers provide a layer of abstraction between the Presenter and the View; they establish the connection between the Presenter's representation of the View and the View's implementation.


Notes On Managed Source Code

All managed source code, such as classes under the ~.client.managed package, are managed by Roo and will be automatically updated when changes are made to any Entity class.  You will need to have Roo up and running inside STS for the automatic updates to take place.  Files under the ~.client.managed package should not be modified manually, except for those inside ~.client.managed.ui package ending with .ui.xml which are UiBinder files and can be changed to rearrange the view.


Notes On Application Performance

The costliest of operations on the client side are those that manipulate the DOM and should be avoided where possible to increase application performance.

Our Roo-generated application creates views as singletons to avoid repeating the costly process of view creation.  On the other hand, Presenters, which are light-weight POJOs, are recreated rather than being cleaned up each time they're recycled.  This is implemented by listening to User events through the registration of a presenter delegate to the view.  view.setDelegate(this) is called from an Activity's start() method and view.setDelegate(null) is called from its onStop() method.


In The Next Post

We are going to make our first attempt at customizing the Roo-generated project by modifying the way GAE authentication currently works.  After our changes, users with GAE administrator privileges and non-admin users will see different content.



IntroPart IPart IIPart IIIPart IVPart V