[UPDATE] The
latest Morjarra release 1.2_13 as well as the
latest Appengine SDK 1.2.2 seem to fix a couple of problems described below. My task backlog is actually growing :-) but I hope I find some time to look again how things work with these new versions.
I spent the last three weeks in a repetition course of the
Swiss army - far away from any Java code. Naturally my fingers started to itch while reading the announcement of Google that their
App Engine now supports Java! So I grabbed my MacBook to enjoy the sun, Java coding and Eastern holidays.
As I usually write web applications with
JBoss Seam, I decided to give the framework a try in the Google cloud - preparing for a bumpy road as Seam founds on standards which are mostly listed as either
not or not known to be working. The following article describes the (bad) tweaks I had to do to get something basic running - I guess if you start doing serious development, you might hit more walls.
First, install the
Eclipse Plugin for App Engine and create a new project. I'll base my descriptions on this initial setup. Switch on sessions in
appengine-web.xml
:
<sessions-enabled>true</sessions-enabled>
Setting up JSF 1.2Download the
latest Mojarra 1.2 release [1.2_12] and put it in the
WEB-INF/lib
directory. You can configure the Faces servlet in web.xml as usual
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.seam</url-pattern>
</servlet-mapping>
Starting Jetty with only this will fail due to some incompatibilities of Jetty and Mojarra. As with Seam we will need JBoss EL anyway, we can configure Mojarra to use the JBoss EL
ExpressionFactory
. Add the JBoss EL JAR to
WEB-INF/lib
and the following XML to your
web.xml
:
<context-param>
<param-name>com.sun.faces.expressionFactory</param-name>
<param-value>org.jboss.el.ExpressionFactoryImpl</param-value>
</context-param>
Now it's already patch time. Jetty in the Google environment seems to have a bug in its Servlet API implementation, missing the
ServletContext.getContextPath()
method (new in version 2.5). Also, Mojarra tries to be clever about initialization work and uses Threads in the
ConfigManager
- the App Engine
SecurityManager
will let the whole thing blow up. Something similar happens in the JBoss
ReferenceCache
class. All patched classes can be found
here. Drop the Java code in your source folder, Jettys classloader will pick it up.
This will at least make the whole thing start up. I also added
facelets (JAR file, view handler in
faces-config.xml
and view suffix in
web.xml
).
Unfortunately, using
RichFaces components is an absolute no go on App Engine. RichFaces is full of references to AWT or JAI classes, which Google blocks completely. If anybody wants to try ICEFaces - good luck!
Adding SeamNow it's time to add all the Seam stuff. Quite a couple of JARs to put into the application. I basically used Seam 2.1.1.GA and libraries from JBoss 4.2.3.GA. The complete list of what should go into
WEB-INF/lib
is shown in the screenshot below.
JTA is not even on the list of APIs Google gives a recommendation, so let's fall back on Java SE behavior. Configure
components.xml
with:
<transaction:entity-transaction entity-manager="#{entityManager}"/>
Although we cannot use Hibernate as persistence provider, Seam has a couple of dependencies to it (e.g. when using Hibernate Validators). As soon as we have it in the classpath, Seam activates its
HibernatePersistenceProvider
component. This will behave pretty bad, and for simplicity we just override this component:
@Name("org.jboss.seam.persistence.persistenceProvider")
@Scope(ScopeType.STATELESS)
@BypassInterceptors
@Install(precedence = Install.APPLICATION,
classDependencies={"org.hibernate.Session", "javax.persistence.EntityManager"})
public class OverrideHibernatePersistenceProvider extends PersistenceProvider {
}
Now also add a persistence provider as described in the
Google docs and disable DataNucleus checking for multiple
PersistenceContextFactory
instantiations with this property in
appengine-web.xml
.
<property name="appengine.orm.disable.duplicate.emf.exception" value="true"/>
As before, also Seam needs a couple of patches. Main reasons for those are references to
javax.naming.NamingException
(which is not white listed, credits to
Toby for the hint) and session/conversation components not implementing
Serializable
correctly. The last point is probably something not hitting Seam or your application the last time.
IdentityAs a next step I tried to add some users to the application. Seam's Identity module builds around the
javax.security.auth.Subject
and
Principal
classes. Even though those classes are
white listed, the
SecurityManager
blocks any attempt to add a Principal to a Subject. Well, how useful is that... As a quick fallback I integrated the
Google Accounts API:
@Name("org.jboss.seam.security.identity")
@Scope(SESSION)
@Install(precedence = Install.APPLICATION)
@BypassInterceptors
@Startup
public class AppEngineIdentity extends Identity {
private static final long serialVersionUID = -9111123179634646677L;
public static final String ROLE_USER = "user";
public static final String ROLE_ADMIN = "admin";
private transient UserService userService;
@Create
@Override
public void create() {
userService = UserServiceFactory.getUserService();
}
@Override
public boolean isLoggedIn() {
return getUserService().isUserLoggedIn();
}
@Override
public Principal getPrincipal() {
if (isLoggedIn())
return new SimplePrincipal(getUserService().getCurrentUser().getNickname());
return null;
}
@Override
public void checkRole(String role) {
if (!isLoggedIn())
throw new NotLoggedInException();
if ((ROLE_ADMIN.equals(role) && !getUserService().isUserAdmin()) || !ROLE_USER.equals(role))
throw new AuthorizationException(String.format(
"Authorization check failed for role [%s]", role));
}
@Override
public boolean hasRole(String role) {
if (!isLoggedIn())
return false;
return ((ROLE_ADMIN.equals(role) && getUserService().isUserAdmin()) || ROLE_USER.equals(role));
}
@Override
public String getUsername() {
if (isLoggedIn())
return getUserService().getCurrentUser().getNickname();
return null;
}
public String createLoginURL(String destination) {
return getUserService().createLoginURL(destination);
}
public String createLogoutURL(String destination) {
return getUserService().createLogoutURL(destination);
}
public User getUser() {
if (isLoggedIn())
return getUserService().getCurrentUser();
return null;
}
private UserService getUserService() {
if (userService == null)
userService = UserServiceFactory.getUserService();
return userService;
}
}
Both
create...
methods can be used in the UI for generating login/logout URLs. Destination defines the URL the user gets redirected after successful login/logout. Also make sure that the
identity
configuration is removed from
components.xml
Wrap upRunning this setup should give you a base for a very simple Seam app like in the screenshot below.
This first step has not been doing any persistence work, and my first tries with DataNucleus were not as straight forward as I had expected from a JPA implementation. Hope Google will catch up here with something more mature. Also, even the simple setup required a couple of nasty tweaks on the frameworks. Another big hurdle here are the runtime differences from production to local environment. For some serious work on App Engine, it's so far more recommendable to look into GWT.
Anyway, if you found this useful - looking forward to hear from your next steps in the cloud.