Thursday, April 16, 2009

JBoss Seam on Google App Engine - First Steps

[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.2

Download 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 Seam

Now 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.

Identity

As 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 up

Running 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.

21 comments:

rey said...

Impressive. I've been waiting for this article to come out after app engine supported java. But I'm wondering how smooth it is to develop seam app on app engine..

Thanks for the article

Micke said...

Hi

I get :
java.security.AccessControlException: access denied (java.lang.RuntimePermission modifyThreadGroup)

From the ReferenceCache class in jboss-el :(

Did you have issues with this?

- Micke

Thomas said...

Hi Micke

Indeed, sounds familiar. Have you integrated my patched version? This one should be free of spawning new threads.

Micke said...

D'Oh

Sorry skimmed over page to quickly.

Thanks,
Micke

mark said...

Hi,

I am trying to use seam on appengine and patched your patches to a working example I made for Icefaces.
But now I got an error Caused by:

java.lang.RuntimeException: Error loading element EntityTransaction with component name null and component class null.

Does this look familiar?

Kind regards,

Mark

Thomas said...

Unfortunately it doesn't. Seam wraps another exception with this runtime exception, so it might be more interesting what else you see in the stacktrace.

junk said...

Hi, great article. I am so close now but I'm getting this exception below. Did you come across this?

RuntimeException: java.lang.ClassNotFoundException:

java.util.concurrent.locks.ReentrantLock$FairSync
at com.google.apphosting.runtime.jetty.AppVersionHandlerMap.handle(AppVersionHandlerMap.java:239)

Thomas said...

It's a known problem in app engine that the inner classes of ReentrantLock are not white listed:

http://code.google.com/p/googleappengine/issues/detail?id=1463

or the issue it has been merged into:

http://code.google.com/p/googleappengine/issues/detail?id=1290

Go and vote for it! :-)

Seam uses ReentrantLock mainly in interceptors and with conversation entries. Following classes keep a reference on a ReentrantLock (Seam 2.2.0.GA):

- BijectionInterceptor
- SynchronizationInterceptor
- ConversationEntry

Maybe that helps you to work around it until Google fixes the problem.

felipenova said...

Hi,
I try everything you said but not work.
Could you send a example code for my e-mail?
If yes, i give my e-mail for you.
Thanks.

Raghunath said...

Your work is so impressive. But seam development is not smooth. Please upload this setup. And It will help us a lot.

raghu

Thomas said...

Hi guys,

ok, will try to put a starter app into our Google code repo within the next weeks which takes some recent fixes on AppEngine/JSF into account. As mentioned in the article, don't expect things to go smooth after that point... ;-)

Will post an update here when done.

Daniel F. M. said...

Could you send a example code for my e-mail?

Thank you.

Thomas said...

Gents,
put my initial sample into the Google repository:

http://ctpjava.googlecode.com/svn/trunk/projects/google-appengine/samples/appengine-sample/

I cleaned out some experimental leftovers via TextMate, so I hope it still compiles :-) Also remember that this is all based on old appengine, JSF and Seam versions. Latest JSF and appengine work together without patches with this context param:

https://javaserverfaces.dev.java.net/nonav/rlnotes/1.2_13/changelog.html

So: no warranties ;-) and no complaints as I will anyway now resume holidays ;-) Anybody interested in upgrading the sample?

Daniel F. M. said...

Hello Thomas, i'm create project[0] with:
JSF: 1.2_13-b01-FCS
Seam: 2.1.1.GA
App Engine SDK: 1.2.5
Spring: 3.0.0.M2
IDE: Eclipse 3.5 (Galileo)

In this project i use one bean with scope type CONVERSATION, which work fine in local machine. but when deploy on appegine don't work fine.

See below my test's:

Test local machine:
Step 1: Go to: http://localhost:8080 and Click button "Novo" (http://seamonappengine.googlecode.com/svn/trunk/war/WEB-INF/logs/local_machine_step1.png)
Step 2: Fill first name with "test" (http://seamonappengine.googlecode.com/svn/trunk/war/WEB-INF/logs/local_machine_step2.png)
Step 3: Confirm clicking button "Confirm" (see "first name" is not empty) (http://seamonappengine.googlecode.com/svn/trunk/war/WEB-INF/logs/local_machine_step3.png)
Step 4: Page with message "saved" is showed (http://seamonappengine.googlecode.com/svn/trunk/war/WEB-INF/logs/local_machine_step4.png)
Logs genered local machine: http://seamonappengine.googlecode.com/svn/trunk/war/WEB-INF/logs/log_local_machine.txt

In local machine Seam with scope type CONVERSATION work fine.

Deploy same application in appengine:
Step 1: Go to: http://1.latest.testeseam1.appspot.com/ and Click button "Novo" (http://seamonappengine.googlecode.com/svn/trunk/war/WEB-INF/logs/appengine_step1.png)
Step 2: Fill first name with "test" (http://seamonappengine.googlecode.com/svn/trunk/war/WEB-INF/logs/appengine_step2.png)
Step 3: Confirm clicking button "Confirm" (see "first name" is empty!) (http://seamonappengine.googlecode.com/svn/trunk/war/WEB-INF/logs/appengine_step3.png)
Step 4: Page inicial is show with first name in blank (http://seamonappengine.googlecode.com/svn/trunk/war/WEB-INF/logs/appengine_step4.png)
Logs genered on appengone: http://seamonappengine.googlecode.com/svn/trunk/war/WEB-INF/logs/log_appengine.txt

[0] - Source code: http://seamonappengine.googlecode.com/svn/trunk
[1] - Site for test: http://1.latest.testeseam1.appspot.com/

You can help me?
Thanks.

Thomas said...

I guess all your conversation components have to be fully serializable as they go into the session. And fully means including every referenced class property.

Daniel F. M. said...

Thomas, you are absolutely right!

I'm change[1] my class AgendaController and now work fine on appengine[2].

[1] - http://code.google.com/p/seamonappengine/source/diff?spec=svn5&r=5&format=side&path=/trunk/src/org/xaloon/gae/demo/wicket/pages/AgendaController.java
[2] - http://3.latest.testeseam1.appspot.com/

Thanks

Gabriel said...

Hi
While I am trying to persist an object, I get:
com.google.appengine.repackaged.com.google.common.base.FinalizableReferenceQueue
INFO: Failed to start reference finalizer thread. Reference cleanup will only occur when new references are created.
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at com.google.appengine.tools.development.agent.runtime.Runtime.invoke(Runtime.java:100)
...
at com.google.appengine.api.datastore.DatastoreServiceImpl.put(DatastoreServiceImpl.java:137)
at org.datanucleus.store.appengine.RuntimeExceptionWrappingDatastoreService.put(RuntimeExceptionWrappingDatastoreService.java:105)
...
at org.datanucleus.store.appengine.DatastorePersistenceHandler.insertObject(DatastorePersistenceHandler.java:225)
at org.datanucleus.state.JDOStateManagerImpl.internalMakePersistent(JDOStateManagerImpl.java:3185)
at org.datanucleus.state.JDOStateManagerImpl.flush(JDOStateManagerImpl.java:4513)
at org.datanucleus.ObjectManagerImpl.flushInternal(ObjectManagerImpl.java:2814)
at org.datanucleus.ObjectManagerImpl.flush(ObjectManagerImpl.java:2754)
at org.datanucleus.ObjectManagerImpl.preCommit(ObjectManagerImpl.java:2893)
at org.datanucleus.TransactionImpl.internalPreCommit(TransactionImpl.java:369)
at org.datanucleus.TransactionImpl.commit(TransactionImpl.java:256)
at org.datanucleus.jpa.EntityTransactionImpl.commit(EntityTransactionImpl.java:104)
at org.jboss.seam.transaction.EntityTransaction.commit(EntityTransaction.java:110)
at org.jboss.seam.jsf.SeamPhaseListener.commitOrRollback(SeamPhaseListener.java:614)
...
Caused by: java.security.AccessControlException: access denied (java.lang.RuntimePermission modifyThreadGroup)
at java.security.AccessControlContext.checkPermission(AccessControlContext.java:323)
at java.security.AccessController.checkPermission(AccessController.java:546)
at java.lang.SecurityManager.checkPermission(SecurityManager.java:532)
at com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkPermission(DevAppServerFactory.java:151)
at com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkAccess(DevAppServerFactory.java:176)
at java.lang.ThreadGroup.checkAccess(ThreadGroup.java:288)
at java.lang.Thread.init(Thread.java:332)
at java.lang.Thread.(Thread.java:419)
at com.google.appengine.repackaged.com.google.common.base.internal.Finalizer.(Finalizer.java:96)
at com.google.appengine.repackaged.com.google.common.base.internal.Finalizer.startFinalizer(Finalizer.java:82)
...


Did anyone meet this issue?
Gabriel.

Sławek Sobótka said...

I rely admire Your effort and patience to struggle with Seam.

I have question about persistence: how to use it?

I'm trying to go standard Seam way: injecting EntityManager managed by Seam to my components and using it. But I see no changes in DB. So i guess that Seam does not manage persistence well.

Do I have to manually handle transactions and close EM? If so, than it may be better to inject EntityManagerFactory maybe?
But doing so cuts off all nice Seam features like conversation scoped EM.

Thomas said...

Hi Slawek

Unfortunately App Engine is so far not a very Seam-friendly environment. Hopefully things get easier with Seam 3 - the Weld-core is already reported to work on App Engine.

Regarding transaction management, did you configure Seam for a non-Java EE environment?

Sławek Sobótka said...

Thomas, thanks for answer. I need to apologize You for bothering abut EM - it was my mistake:)
CRUD works fine on appengine-sample.

Weld is not enough when developer get used to all Seam goodies:)

Did You have any significant blocking problem going further with Seam on GAE?

What is the most convenient in Seam is (imho): EM management and transactions, bijection, Page and Conversation scopes, security, some king of REST-orientation for old JSF.
I don't need ajax, jbpm, drools etc.

Have You found any problems in that?
Is it worth to give a chance for Seam in cloud (assuming lack of time for mistake:)?

Thomas said...

Hi Slavek

I personally will wait for Seam 3 before I spend more time on App Engine (mainly for the timing issue you mention ;-).

If you stick to the features you mentioned, it might turn out ok (although one post above hinted problems with Conversations). But you should not be afraid of digging into source code, either JSF or Seam or its dependencies.

BTW, even though there are a few goodies less, it's definitely worth to look at Weld resp. CDI (JSR-299). The possibilities you have now with Java EE 6 are really impressive!