GWT Login Security

In the previous post we added support for importing CSV files into Serendipity by taking advantage of Apache FileUpload and opencsv. In this post, we're going to add support for login security to Serendipity.

In this post, we'll:

  1. Update Serendipity's 'Sign in' page
  2. Store user information in a secure fashion on the server
  3. Add a LoggedIn ActionValidator to Serendipity's Action Handlers
  4. Add a LoggedIn Gatekeeper to Serendipity's Presenters
  5. Add protection against XSRF attacks
  6. Test the application in development mode

Note: You can download the sample application from the CRMdipity (Se-r-en-dipity) project's download page.

1. Update Serendipity's 'Sign in' page

Serendipty's Sign in page provides support for user/password authentication. It contains a TextBox for the user name and a PasswordTextBox for the password.

 

Let's take a look at the SignInPage View:

...

public class SignInPageView extends ViewWithUiHandlers<SignInPageUiHandlers> implements
    SignInPagePresenter.MyView {

  private static final String DEFAULT_USER_NAME = "Administrator";

  private static String html = "<div>\n"
    + "<table align=\"center\">\n"
    + "  <tr>\n" + "<td> </td>\n" + "<td> </td>\n" + "</tr>\n"
    + "  <tr>\n" + "<td> </td>\n" + "<td> </td>\n" + "</tr>\n"
    + "  <tr>\n" + "<td> </td>\n" + "<td> </td>\n" + "</tr>\n"
    + "  <tr>\n" + "<td> </td>\n" + "<td> </td>\n" + "</tr>\n"
    + "  <tr>\n" + "<td> </td>\n" + "<td> </td>\n" + "</tr>\n"
    + "  <tr>\n"
    + "    <td colspan=\"2\" style=\"font-weight:bold;\">Sign In <img src=\"images/signin.gif\"/></td>\n"
    + "  </tr>\n"
    + "  <tr>\n"
    + "    <td>User name</td>\n"
    + "    <td id=\"userNameFieldContainer\"></td>\n"
    + "  </tr>\n"
    + "  <tr>\n"
    + "    <td>Password</td>\n"
    + "    <td id=\"passwordFieldContainer\"></td>\n"
    + "  </tr>\n"
    + "  <tr>\n"
    + "    <td></td>\n"
    + "    <td id=\"signInButtonContainer\"></td>\n"
    + "  </tr>\n"
    + "  <tr>\n" + "<td> </td>\n" + "<td> </td>\n" + "</tr>\n"
    + "  <tr>\n"
    + "    <td colspan=\"2\">Forget your password?</td>\n"
    + "  </tr>\n"
    + "  <tr>\n"
    + "    <td colspan=\"2\">Contact your Serendipity administrator.</td>\n"
    + "  </tr>\n"
    + "</table>\n"
    + "</div>\n";

  HTMLPanel panel = new HTMLPanel(html);

  private final TextBox userNameField;
  private final PasswordTextBox passwordField;
  private final Button signInButton;

  @Inject
  public SignInPageView() {
    userNameField = new TextBox();
    passwordField = new PasswordTextBox();
    signInButton = new Button("Sign in");

    userNameField.setText(DEFAULT_USER_NAME);

    panel.add(userNameField, "userNameFieldContainer");
    panel.add(passwordField, "passwordFieldContainer");
    panel.add(signInButton, "signInButtonContainer");

    bindCustomUiHandlers();
  }

  protected void bindCustomUiHandlers() {

    signInButton.addClickHandler(new ClickHandler() {
      public void onClick(ClickEvent event) {

        if (FieldVerifier.isValidUserName(getUserName()) &&
           (FieldVerifier.isValidPassword(getPassword()))) {
          if (getUiHandlers() != null) {
            getUiHandlers().onOkButtonClicked();
          }
        }
        else {
          event.cancel();
          SC.say("Sign in", "You must enter a valid User name and Password.");
          resetAndFocus();
        }
      }
    });
  }

  @Override
  public Widget asWidget() {
    return panel;
  }

  @Override
  public String getUserName() {
    return userNameField.getText();
  }

  @Override
  public String getPassword() {
    return passwordField.getText();
  }

  @Override
  public void resetAndFocus() {
    userNameField.setFocus(true);
    userNameField.selectAll();
  }
}

And you'll notice that the constructor initialises the user name TextBox, the password PasswordTextBox and the Sign in button and then calls the bindCustomUiHandlers method. In the bindCustomUiHandlers method we register a click handler for the Sign in button that is responsible for validating the user name and password.

The FieldVerifier class:

...

public class FieldVerifier {

  public static boolean isValidUserName(String name) {

    if (name == null) {
      return false;
    }

    return name.length() > 6;
  }

  /*

  (                         # Start of group
      (?=.*\d)              #   must contains one digit from 0-9
      (?=.*[a-z])           #   must contains one lower case characters
      (?=.*[A-Z])           #   must contains one upper case characters
      (?=.*[@#$%])          #   must contains one special symbols in the list "@#$%"
                  .         #     match anything with previous condition checking
                    {8,32}  #        length at least 8 characters and maximum of 32
    )                       # End of group

  */

  private final static String PASSWORD_VALIDATION_REGEX = "((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{8,32})";

  public static boolean isValidPassword(String password) {

    if (password == null) {
      return false;
    }

    return password.matches(PASSWORD_VALIDATION_REGEX);
  }
}

User names must contain a minimum of 7 characters. Passwords must contain at least 8 characters with at least one digit, one upper case letter, one lower case letter and one special symbol (e.g. "N0More$ecrets").

The SignInPage Presenter:

...

public class SignInPagePresenter extends
    Presenter<SignInPagePresenter.MyView, SignInPagePresenter.MyProxy> implements
        SignInPageUiHandlers {

  private final EventBus eventBus;
  private final DispatchAsync dispatcher;
  private final PlaceManager placeManager;

  // don't forget to update SerendipityGinjector & ClientModule
  @ProxyStandard
  @NameToken(NameTokens.signInPage)
  @NoGatekeeper
  public interface MyProxy extends Proxy<SignInPagePresenter>, Place {
  }

  public interface MyView extends View, HasUiHandlers<SignInPageUiHandlers> {
    String getUserName();
    String getPassword();
    void resetAndFocus();
  }

  @Inject
  public SignInPagePresenter(final EventBus eventBus, MyView view, MyProxy proxy,
      final DispatchAsync dispatcher, final PlaceManager placeManager) {
    super(eventBus, view, proxy);

    getView().setUiHandlers(this);

    this.eventBus = eventBus;
    this.dispatcher = dispatcher;
    this.placeManager = placeManager;
  }

  @Override
  protected void onReset() {
    super.onReset();
    getView().resetAndFocus();
  }

  @Override
  protected void revealInParent() {
    RevealRootContentEvent.fire(this, this);
 }

  @Override
  public void onOkButtonClicked() {
    sendCredentialsToServer();
  }

  private void sendCredentialsToServer() {
    String login = getView().getUserName();
    String password = getView().getPassword();

    getDispatcher().execute(new LoginAction(login, password),
        new AsyncCallback<LoginResult>() {
        
      @Override
      public void onFailure(Throwable caught) {
        String message = "onFailure() - " + caught.getLocalizedMessage();
        
        if (caught instanceof LoginException) {
          message = "onFailure() - " + "Invalid User name or Password." ;
        }
          
        getView().resetAndFocus();
        
        Log.debug(message);
      }

      @Override
      public void onSuccess(LoginResult result) {
        CurrentUser currentUser = new CurrentUser(getView().getUserName());
        
        LoginAuthenticatedEvent.fire(eventBus, currentUser);
        
        PlaceRequest placeRequest = new PlaceRequest(NameTokens.mainPage);
        getPlaceManager().revealPlace(placeRequest);       
      }
    });
  }

  private DispatchAsync getDispatcher() {
    return dispatcher;
  }

  private PlaceManager getPlaceManager()  {
    return placeManager;
  }
}

The Sign in button's click handler calls the Presenter's onOkButtonClicked UI handler. The onOkButtonClicked UI handler then calls the sendCredentialsToServer method which invokes the GWTP dispatcher which forwards the LoginAction to the Login Handler.

We can use gwt-platform's @GenDispatch annotation to generate the 'Login' Action and Result classes:

package au.com.uptick.serendipity.shared.action;

import com.gwtplatform.annotation.GenDispatch;
import com.gwtplatform.annotation.In;
import com.gwtplatform.annotation.Out;
import com.gwtplatform.dispatch.shared.UnsecuredActionImpl;

@GenDispatch(isSecure = false, serviceName = UnsecuredActionImpl.DEFAULT_SERVICE_NAME)
public class Login {

  @In(1) String login;
  @In(2) String password;
  @Out(1) String sessionKey;

}

2. Store user information in a secure fashion on the server

In order to setup a new account a user (or the system administrator) must specify a user name and a password. We need to store this information so that we can authenticate future login requests. However, passwords are secrets so they must never be stored as plain text. That's why most password authentication schemes take advantage of both cryptographic hash functions and password salting.

The Login Handler implementation:

...

public class LoginHandler implements ActionHandler<LoginAction, LoginResult> {

  private final Provider<HttpServletRequest> requestProvider;

  @Inject
  LoginHandler(final ServletContext servletContext,
      final Provider<HttpServletRequest> requestProvider) {
    this.requestProvider = requestProvider;
  }

  @Override
  public LoginResult execute(final LoginAction action,
      final ExecutionContext context) throws ActionException {

    LoginResult result = null;
    UserDao userDao = new UserDao();

    try {
      User user = userDao.retrieveUser(action.getLogin());

      if (user != null && isValidLogin(action, user)) {

        HttpSession session = requestProvider.get().getSession();
        session.setAttribute("login.authenticated", action.getLogin());

        result = new LoginResult(session.getId());
      }
      else {
        throw new ActionException("Invalid User name or Password.");
      }
    }
    catch (Exception e) {
      throw new ActionException(e);
    }

    return result;
  }

  private Boolean isValidLogin(LoginAction action, User user) {
    String hash = Security.sha256(user.getSalt() + action.getPassword());

    return hash.equals(user.getPassword());
  }

  @Override
  public Class<LoginAction> getActionType() {
    return LoginAction.class;
  }

  @Override
  public void undo(LoginAction action, LoginResult result,
      ExecutionContext context) throws ActionException {
  }
}

The Login Handler uses the credentials included in the LoginAction to authenticate the user. The stored 'password' (a hash of the salt and the password) is compared to the credentials the user provided and if the credentials match then the authentication process has been succcessful.

The User class:

...

@Entity
@Table(name = EntityTokens.USER_TABLE)
public class User {

  @Id
  @Column(name = EntityTokens.LOGIN_COLUMN, length = EntityTokens.LOGIN_COLUMN_LENGTH)
  protected String login;

  @Column(name = EntityTokens.SALT_COLUMN, length = EntityTokens.SALT_COLUMN_LENGTH)
  protected String salt;

  @Column(name = EntityTokens.PASSWORD_COLUMN, length = EntityTokens.PASSWORD_COLUMN_LENGTH)
  protected String password;

  // JPA requires a no-argument constructor
  public User() {
  }

  public User(String login, String salt, String password) {
    this.login = login;
    this.salt = salt;
    this.password = password;
  }

  public String getLogin() {
    return login;
  }

  public void setLogin(String login) {
    this.login = login;
  }

  public String getSalt() {
    return salt;
  }

  public void setSalt(String salt) {
    this.salt = salt;
  }

  public String getPassword() {
    return password;
  }

  public void setPassword(String password) {
    this.password = password;
  }

  public boolean isAccountNonExpired() {
    return true;
  }

  public boolean isAccountNonLocked() {
    return true;
  }

  public boolean isCredentialsNonExpired() {
    return true;
  }

  public boolean isEnabled() {
    return true;
  }

...

The UserDao class:

...

public class UserDao extends BaseDao {

  public Long createObject(Object object) {
    return null;
  }

  @Override
  public List<Object> retrieveObjects(int maxResults, int firstResult) {
    return null;
  }

  public String createUser(User user) {

    EntityManager em = createEntityManager();
    EntityTransaction tx = em.getTransaction();
    String login = "";

    try {
      tx.begin();
      em.persist(user);
      login = user.getLogin();
      tx.commit();
    }
    catch (Throwable t) {
      t.printStackTrace();
      tx.rollback();
    }
    finally {
      em.close();
    }

    return login;
  }

  public User retrieveUser(String login) {

    EntityManager em = createEntityManager();
    User user = null;

    try {
      TypedQuery<User> query = em.createQuery("select u from User u where u.login = ?1", User.class);
      query.setParameter(1, login);
      user = query.getSingleResult();
    }
    finally {
      em.close();
    }

    return user;
  }
}

3. Add a LoggedIn ActionValidator to Serendipity's Action Handlers

GWTP provides a convenient server-side mechanism, ActionValidators, that we can use to determine if a user can execute an action.

The LoggedIn ActionValidator:

...

public class LoggedInActionValidator implements ActionValidator  {
  
  private final Provider<HttpServletRequest> requestProvider;

  @Inject
  LoggedInActionValidator(final Provider<HttpServletRequest> requestProvider) {
    this.requestProvider = requestProvider;
  }

  @Override
  @Singleton
  public boolean isValid(Action<? extends Result> action) {
    boolean result = true;

    HttpSession session = requestProvider.get().getSession();

    Object authenticated = session.getAttribute("login.authenticated");

    if (authenticated == null) {
      result = false;
    }
    
    return result;
  }
}

And, the updated ServerModule with the LoggedIn ActionValidator bound to Serendipity's ActionHandlers:

...

public class ServerModule extends HandlerModule {

  @Override
  protected void configureHandlers() {

    // Bind Action to Action Handler
    bindHandler(LoginAction.class, LoginHandler.class);

    // Bind Action to Action Handler and Action Validator
    bindHandler(RetrieveAccountsAction.class, RetrieveAccountsHandler.class, LoggedInActionValidator.class);
    
    bindHandler(CreateAccountAction.class, CreateAccountHandler.class, LoggedInActionValidator.class);
    bindHandler(RetrieveAccountAction.class, RetrieveAccountHandler.class, LoggedInActionValidator.class);
    bindHandler(UpdateAccountAction.class, UpdateAccountHandler.class, LoggedInActionValidator.class);
    bindHandler(DeleteAccountAction.class, DeleteAccountHandler.class, LoggedInActionValidator.class);
    
    bindHandler(RetrieveReportsAction.class, RetrieveReportsHandler.class, LoggedInActionValidator.class);
  }
}

4. Add a LoggedIn Gatekeeper to Serendipity's Presenters

GWTP provides a client-side mechanism, Gatekeepers, that we can use to prevent presenters from revealing themselves.

The LoggedIn Gatekeeper:

...

public class LoggedInGatekeeper implements Gatekeeper {
  
  private final EventBus eventBus;

  private CurrentUser currentUser = null;

  @Inject
  public LoggedInGatekeeper(final EventBus eventBus) {
    this.eventBus = eventBus;
    
    this.eventBus.addHandler(LoginAuthenticatedEvent.getType(), new LoginAuthenticatedEventHandler() {
      @Override
      public void onLogin(LoginAuthenticatedEvent event) {
        
        currentUser = event.getCurrentUser();

        Log.debug(currentUser.getLogin() + " credentials have been authenticated.");
      }
    });
  }

  @Override
  public boolean canReveal() {
    boolean loggedIn = false;
    
    if (currentUser != null) {
      loggedIn = currentUser.isLoggedIn();
    }
    
    return loggedIn;
  }
}

Take a look at the LoggedInGatekeeper's canReveal method and you will notice that it returns false until the LoggedInGatekeeper has received a 'LoginAuthenticated' event from the SignInPage Presenter.

Now all we need to do is add the @UseGatekeeper annotation to the proxy of each presenter we want to protect:

...

  @ProxyCodeSplit
  @NameToken(NameTokens.mainPage)
  @UseGatekeeper(LoggedInGatekeeper.class)
  public interface MyProxy extends Proxy<MainPagePresenter>, Place {
  }

...

The LoginAuthenticatedEvent class:

package au.com.uptick.serendipity.client.presenter;

import au.com.uptick.serendipity.client.CurrentUser;

import com.google.gwt.event.shared.GwtEvent;
import com.gwtplatform.mvp.client.EventBus;

public class LoginAuthenticatedEvent extends GwtEvent<LoginAuthenticatedEventHandler> {

  private static final Type<LoginAuthenticatedEventHandler> TYPE = new Type<LoginAuthenticatedEventHandler>();

  public static Type<LoginAuthenticatedEventHandler> getType() {
    return TYPE;
  }

  public static void fire(EventBus eventBus, CurrentUser currentUser) {
    eventBus.fireEvent(new LoginAuthenticatedEvent(currentUser));
  }

  private final CurrentUser currentUser;

  public LoginAuthenticatedEvent(CurrentUser currentUser) {
    this.currentUser = currentUser;
  }

  public CurrentUser getCurrentUser() {
    return currentUser;
  }

  @Override
  protected void dispatch(LoginAuthenticatedEventHandler handler) {
    handler.onLogin(this);
  }

  @Override
  public Type<LoginAuthenticatedEventHandler> getAssociatedType() {
    return getType();
  }
}

5. Add protection against XSRF attacks

To protect against XSRF attacks we need to bind a string constant annotated with @SecurityCookie on both the client and on the server. On the client, we can do this in the configure method of the ClientModule:

...

public class ClientModule extends AbstractPresenterModule {

  @Override
  protected void configure() {

    // Protect against XSRF attacks - securityCookieName = "JSESSIONID";
    bindConstant().annotatedWith(SecurityCookie.class).to(SharedTokens.securityCookieName);

    // Singletons
    bind(EventBus.class).to(DefaultEventBus.class).in(Singleton.class);
    bind(PlaceManager.class).to(SerendipityPlaceManager.class).in(Singleton.class);
    bind(TokenFormatter.class).to(ParameterTokenFormatter.class).in(Singleton.class);
    bind(RootPresenter.class).asEagerSingleton();
    bind(ProxyFailureHandler.class).to(DefaultProxyFailureHandler.class).in(Singleton.class);
    
    bind(LoggedInGatekeeper.class).in(Singleton.class);
    bind(AdminGatekeeper.class).in(Singleton.class);

    // Constants
    // Bind the Sign In page to the default place
    bindConstant().annotatedWith(DefaultPlace.class).to(NameTokens.signInPage);

    ...

  }
}

On the server, we can do it in the bindConstants method of the DispatchServletModule:

...

public class DispatchServletModule extends ServletModule {

  @Override
  public void configureServlets() {
    bindConstants();
    bindFilters();
    bindServlets();
  }

  protected void bindConstants() {
    // Protect against XSRF attacks - securityCookieName = "JSESSIONID";
    bindConstant().annotatedWith(SecurityCookie.class).to(SharedTokens.securityCookieName);
  }

  protected void bindFilters() {
  }

  // <!--
  // This Guice listener hijacks all further filters and servlets.
  // Extra filters and servlets have to be configured in your
  // ServletModule#configureServlets() by calling
  // serve(String).with(Class<? extends HttpServlet>) and
  // filter(String).through(Class<? extends Filter).
  // -->

  public static final String DEFAULT_SERVICE_PATH = "/";
  public static final String REPORTS_SERVICE_PATH = "/reports/*";
  public static final String FILE_UPLOAD_SERVICE_PATH = "/upload";
  public static final String FILE_DOWNLOAD_SERVICE_PATH = "/download/*";

  protected void bindServlets() {
    // For GWT 2.1.1
    serve(DEFAULT_SERVICE_PATH + ActionImpl.DEFAULT_SERVICE_NAME).with(
        DispatchServiceImpl.class);

    // This registers a servlet (subclass of HttpServlet) called ReportServlet
    // to serve any web requests using a path-style syntax (as you would in web.xml).
    serve(REPORTS_SERVICE_PATH).with(ReportServlet.class);
    serve(FILE_UPLOAD_SERVICE_PATH).with(FileUploadServlet.class);
    serve(FILE_DOWNLOAD_SERVICE_PATH).with(FileDownloadServlet.class);
  }
}

We also need to ensure our Action's (e.g. RetrieveAccounts, CreateAccount, DeleteAccount, etc.) isSecured method returns true (isSecure = true) :

import java.util.List;

import com.gwtplatform.annotation.GenDispatch;
import com.gwtplatform.annotation.In;
import com.gwtplatform.annotation.Out;
import com.gwtplatform.dispatch.shared.ActionImpl;

import au.com.uptick.serendipity.shared.dto.sales.AccountsDto;

@GenDispatch(isSecure = true, serviceName = ActionImpl.DEFAULT_SERVICE_NAME)
public class RetrieveAccounts {

  @In(1) int maxResults;
  @In(2) int firstResult;

  @Out(1) List<AccountsDto> accountDtos;

}

6. Test the application in development mode

At this point, you should be able to compile Serendipity and launch it from within Eclipse.

You can use the updated AccountTestCase to populate the 'User' table and don't forget to update the "hibernate.hbm2ddl.auto" property (e.g. run the JUnit test with value="create" then comment it out) in the persistence.xml file.

The updated AccountTestCase:

...

  public void createUsers() {

    User user1 = new User();
    user1.setLogin("Administrator");

    String salt = Security.randomCharString();

    String password = "N0More$ecrets";
    String hash = Security.sha256(salt + password);

    user1.setSalt(salt);
    user1.setPassword(hash);

    UserDao userDao = new UserDao();
    userDao.createUser(user1);
  }

...

Note: You can download the sample application from the CRMdipity (Se-r-en-dipity) project's download page.

What's Next

At this point, we've learnt how to add support for login security to Serendipity. Next we'll look at how to add interactive charting to Serendipity.

SmartGWT and FusionCharts

Comments

*Result.java files missing

In my download of Serendipity 0.5 none of the *Result.java files appear in the Eclipse project for some reason.

When I went into the shell I could find them, but they are invisible to Eclipse.

Thank-you for producing an excellent resource.

Annotation processing

Hi,

You just need to enable annotation processing within Eclipse.

Take a look at the GWTP wiki re "Configuring your build environment".

Cheers
Rob

Login Issue

After importing Serendipity-0.6.0 into Eclipse and running it in Production mode, their seems to be a problem in the Login module.

Is there a default login/password? And where can I find the registered login/password?

Thanks for this tutorial. You're doing a hell of a job.

Re: Login Issue

Hi,

Use the AccountTestCase to populate the 'User', 'Account' and 'Report' tables (see step 6) and don't forget to update the "hibernate.hbm2ddl.auto" property (e.g. run the JUnit test with value="create" then comment it out) in the persistence.xml file.

You should then be able to compile Serendipity and launch it from within Eclipse.

The createUsers() method in the AccountTestCase will create a new user with Login = "Administrator" and Password = "N0More$ecrets".

Cheers
Rob

Re: Login Issue

Environment: Eclipse, Serendipity 0.6, Mac Os X Ran JUnit test against AccountTestCase and got the following exceptions: Any help?

Thanks,
-Guoqian

P.S. java.lang.ExceptionInInitializerError at au.com.uptick.serendipity.server.AccountTestCase.createUsers(AccountTestCase.java:93) at au.com.uptick.serendipity.server.AccountTestCase.testAccountDao(AccountTestCase.java:66)
...
org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197) Caused by: javax.persistence.PersistenceException: [PersistenceUnit: au.com.uptick.serendipity] Unable to configure EntityManagerFactory
...

Re: Login Issue

Hi,

Can you raise an issue, please -> http://code.google.com/p/crmdipity/issues/list

And, provide some more information re what you where doing + Eclispe version (e.g. Helios) and JVM (e.g. 1.6.26).

Cheers
Rob

AdminGatekeeper question

Hi,

I've noticed the existence of two classes that extend the Gatekeeper class (AdminGatekeeper & LoggedInGatekeeper). Is it really necessary to have them both 'cause they look very similiar to me (identitical to be accurate :))

Thanks

Re: AdminGatekeeper question

Hi,

I haven't as yet added support for role-based access control, however, the AdminGatekeeper should make a call to isUserInRole("Administrator") or isAdministrator() in the AdminGatekeeper's canReveal method. For example:

  ->
  @Override
  public boolean canReveal() {
    boolean administrator = false;
    
    if (currentUser != null) {
      administrator = currentUser.isUserInRole("Administrator");
    }
    
    return administrator;
  } 

  ->

The AdminGatekeeper could then be used to protect Presenters that should only be accessed by "Administrator" users.

Cheers
Rob

Create Tables (via DDL) in HSQLDB

Is there any code to create the schema for HSQLDB?

I found the AccountTestCase which can populate the tables, but the tables do not exist, so I got a lot of errors like: "java.sql.SQLException: user lacks privilege or object not found: USER".

I have a .NET background but I'm relative new to Java. It's best that I can get something up and running before I can learn it easier.

Thank you!

Create Tables (via DDL) in HSQLDB

Hi,

You can use the AccountTestCase to create the Serendipity database (actually a catalogue that consists of between 2 and 6 files) and to populate the 'User', 'Account' and 'Report' tables (see step 6). Don't forget to update the "hibernate.hbm2ddl.auto" property (e.g. run the JUnit test with value="create" then comment it out) in the persistence.xml file.

You should then be able to compile Serendipity and launch it from within Eclipse.

If you haven't already then you should take a look at the HSQLDB Users Guide. In particular page 21 which states:

"When a server instance is started, or when a connection is made to an in-process database, a new, empty database is created if no database exists at the given path."

Which can be confusing for new users.

If you are looking for the DDL for the schema then take a look at the serendipitydb.log file and the HSQLDB Utility Guide which describes how to use the HSQLDB sqltool.

Cheers
Rob

Update of Serendipity

Hi,

Congratulations on a fantastic series of tutorials on Smart GWT and GWT.

I've been looking for good examples for some time before discovering yours. I've installed GWT 2.2.0, GWTP 0.5.1, and Smart GWT 2.4 but when I run the app it doesn't respond properly to mouse clicks so I can't navigate within it. I am putting this down to having to use versions of IE and Chrome that are not supported by the older versions of gwt etc.

What would I need to update in Serendipity to use it under the latest versions of GWT, Smart GWT etc? or is there another application that is more up to date that I could look at?

Again, thanks for the fantastic work.

Colin Hedley

Hi, Thanks for the positive

Hi,

Thanks for the positive feedback.  I wrote the posts in a step-by-step style because that was what I was looking for and couldn't find :-)

One of the reasons I switched to using Maven was so that I could keep up with all the changes (e.g. new versions, etc.) to each of the components (e.g. GWT, Smart GWT, Gin, Guice, etc.).

There are now GWT, Smart GWT and Ext GWT samples:

-> http://code.google.com/p/gwt-cx/

and all the source in in SVN:

-> http://code.google.com/p/gwt-cx/source/browse/

Cheers
Rob