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:
- Update Serendipity's 'Sign in' page
- Store user information in a secure fashion on the server
- Add a LoggedIn ActionValidator to Serendipity's Action Handlers
- Add a LoggedIn Gatekeeper to Serendipity's Presenters
- Add protection against XSRF attacks
- 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.
- Tags:


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