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



11 comments:

  1. Can't wait for your next post.

    Your posts really helped a lot.

    Thanks!

    ReplyDelete
  2. Hi, really great work you are doing!
    I'm trying to modify the Pizza entity adding an image field (Byte[] image) but something is going wrong over the generated code.
    Am I doing something wrong? Or just am I trying to do something still not supported in roo-gwt project?

    ReplyDelete
  3. Nicely done!
    Would be great to see a demo version of the application (running... maybe in a video)

    ReplyDelete
  4. Hi, thanks for this blog. I am also trying to customize my roo gwt project. Using 'new GaeLoginWidgetDriver(requestFactory)...' results in a compilation error, because the constructor of GaeLoginWidgetDriver only accepts objects of type 'MakesGaeRequests', but you use 'ApplicationRequestFactory' instead. What can I do?

    ReplyDelete
  5. Enrico,

    The following link might help you:
    http://viralpatel.net/blogs/2011/02/spring-roo-save-read-blob-object-spring-roo-tutorial.html

    Cristian,
    I have posted the source code and a link to the deployed app above. See: http://gwtsts.blogspot.com/2011/03/part-iv-customization-of-app-engine.html#source

    Manuel,
    ApplicationRequestFactory interface extends the MakesGaeRequests interface. There must be another reason why the compiler is failing. Compare your source code with the one posted above: source

    ReplyDelete
  6. My sources say that ApplicationRequestFactory does not extend MakesGaeRequests, but ScaffoldRequestFactory which extends RequestFactory. Maybe I am using old versions (roo version 1.1.1 and GWT 2.1.1). I´m gonna try latest version...

    ReplyDelete
  7. I´ve downloaded the latest roo version 1.1.2, but I did not get my problem to fix. I can not access to your source link because of firewall settings at my office. So I will compare sources when I´m back home tonight. Anyway thanks for your reply! :)

    ReplyDelete
  8. Try regenerating the scaffold app with Roo 1.1.3. You can download the 1.1.3 snapshot build that I have been using from here.

    ReplyDelete
  9. hi cengiz,

    the application at pizzashopexample.appspot.com is somehow taking my login data from the necessary google login, so it is not possible to test the "admin" part. after clicking the "sign out" link, i'm getting fully logged out from my google account (:

    ReplyDelete
  10. Yes, you would need to have administrator privileges to the deployed GAE project to be able to view the admin console link or the console itself. To test the admin functionality, you'll need to deploy your own copy to App Engine.

    ReplyDelete