Upgrading to JSF 2
Last week, I spent a few hours upgrading AppFuse from JSF 1.2 to JSF 2.0. In reality, I upgraded from MyFaces 1.2.7 to 2.0.4, but all JSF implementations should be the same, right? All in all, it was a pretty easy upgrade with a few minor AppFuse-specific things. My goal in upgrading was to do the bare minimum to get things working and to leave integration of JSF 2 features for a later date.
In addition to upgrading MyFaces, I had to upgrade Tomahawk by changing the dependency's artifactId to tomahawk20. I was also able to remove the following listener from my web.xml:
<listener> <listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class> <listener>
After that, I discovered that MyFaces uses a new URI (/javax.faces.resource/) for serving up some of its resource files. I kindly asked Spring Security to ignore these requests by adding the following to my security.xml file.
<intercept-url pattern="/javax.faces.resource/**" filters="none"/>
Since JSF 2 includes Facelets by default, I tried removing Facelets as a dependency. After doing this, I received the following error:
ERROR [308855416@qtp-120902214-7] ViewHandlerWrapper.fillChain(158) | Error instantiation parent Faces ViewHandler java.lang.ClassNotFoundException: com.sun.facelets.FaceletViewHandler at org.codehaus.plexus.classworlds.strategy.SelfFirstStrategy.loadClass(SelfFirstStrategy.java:50) at org.codehaus.plexus.classworlds.realm.ClassRealm.loadClass(ClassRealm.java:244) at org.codehaus.plexus.classworlds.realm.ClassRealm.loadClass(ClassRealm.java:230) at org.mortbay.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:401) at org.mortbay.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:363) at org.ajax4jsf.framework.ViewHandlerWrapper.fillChain(ViewHandlerWrapper.java:144) at org.ajax4jsf.framework.ViewHandlerWrapper.calculateRenderKitId(ViewHandlerWrapper.java:68) at org.apache.myfaces.lifecycle.DefaultRestoreViewSupport.isPostback(DefaultRestoreViewSupport.java:179) at org.apache.myfaces.lifecycle.RestoreViewExecutor.execute(RestoreViewExecutor.java:113) at org.apache.myfaces.lifecycle.LifecycleImpl.executePhase(LifecycleImpl.java:171) at org.apache.myfaces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:118) at javax.faces.webapp.FacesServlet.service(FacesServlet.java:189)
Figuring this was caused by the following element in my web.xml ...
<context-param> <param-name>org.ajax4jsf.VIEW_HANDLERS</param-name> <param-value>com.sun.facelets.FaceletViewHandler</param-value> </context-param>
... I removed it and tried again. This time I received a NoClassDefFoundError:
java.lang.NoClassDefFoundError: com/sun/facelets/tag/TagHandler at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) at java.lang.ClassLoader.defineClass(ClassLoader.java:616) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:141) at java.net.URLClassLoader.defineClass(URLClassLoader.java:283) at java.net.URLClassLoader.access$000(URLClassLoader.java:58) at java.net.URLClassLoader$1.run(URLClassLoader.java:197) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:190) at org.mortbay.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:392) at org.mortbay.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:363) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:247) at org.apache.myfaces.shared_impl.util.ClassUtils.classForName(ClassUtils.java:184) at org.apache.myfaces.view.facelets.util.ReflectionUtil.forName(ReflectionUtil.java:67)
Since everything seemed to work with Facelets in the classpath, I decided to save this headache for a later date. I entered two issues in AppFuse's JIRA, one for removing Facelets and one for replacing Ajax4JSF with RichFaces.
The next issue I encountered was redirecting from AppFuse's password hint page. The navigation-rule for this page is as follows:
<navigation-rule> <from-view-id>/passwordHint.xhtml</from-view-id> <navigation-case> <from-outcome>success</from-outcome> <to-view-id>/login</to-view-id> <redirect/> </navigation-case> </navigation-rule>
With JSF 2.0, the rule changes the URL to /login.xhtml when redirecting (where it was left as /login with 1.2) and it was caught by the security setting in my web.xml that prevents users from viewing raw templates.
<security-constraint> <web-resource-collection> <web-resource-name>Protect XHTML Templates</web-resource-name> <url-pattern>*.xhtml</url-pattern> </web-resource-collection> <auth-constraint/> </security-constraint>
To solve this issue, I had to make a couple of changes:
- Comment out the security-constraint in web.xml and move it to Spring Security's security.xml file.
<intercept-url pattern="/**/*.xhtml" access="ROLE_NOBODY"/>
- Add a rule to urlrewrite.xml that redirects back to login (since login.xhtml doesn't exist and I'm using extensionless URLs).
<rule match-type="regex"> <from>^/login.xhtml$</from> <to type="redirect">%{context-path}/login</to> </rule>
After getting the Password Hint feature passing in the browser, I tried running the integration tests (powered by Canoo WebTest). The Password Hint test kept failing with the following error:
[ERROR] /Users/mraible/dev/appfuse/web/jsf/src/test/resources/web-tests.xml:51: JavaScript error loading page http://localhost:9876/appfuse-jsf-2.1.0-SNAPSHOT/passwordHint?username=admin: syntax error (http:// localhost:9876/appfuse-jsf-2.1.0-SNAPSHOT/javax.faces.resource/oamSubmit.js.jsf?ln=org.apache.myfaces#122)
Figuring this was caused by my hack to submit the form when the page was loaded, I turned to Pretty Faces, which allows you to call a method directly from a URL. After adding the Pretty Faces dependencies to my pom.xml, I created a src/main/webapp/WEB-INF/pretty-config.xml file with the following XML:
<url-mapping> <pattern value="/editProfile"/> <view-id value="/userForm.jsf"/> <action>#{userForm.edit}</action> </url-mapping> <url-mapping> <pattern value="/passwordHint/#{username}"/> <view-id value="/passwordHint.jsf"/> <action>#{passwordHint.execute}</action> </url-mapping>
This allowed me to remove both editProfile.xhtml and passwordHint.xhtml, both of which simply auto-submitted forms.
At this point, I figured I'd be good to go and ran my integration tests again. The first thing I discovered was that ".jsf" was being tacked onto my pretty URL, most likely by the UrlRewriteFilter. Adding the following to my PasswordHint.java class solved this.
if (username.endsWith(".jsf")) { username = username.substring(0, username.indexOf(".jsf")); }
The next thing was a cryptic error that took me a while to figure out.
DEBUG [1152467051@qtp-144702232-0] PasswordHint.execute(38) | Processing Password Hint... 2011-03-05 05:48:52.471:WARN::/passwordHint/admin com.ocpsoft.pretty.PrettyException: Exception occurred while processing <:#{passwordHint.execute}> null at com.ocpsoft.pretty.faces.beans.ActionExecutor.executeActions(ActionExecutor.java:71) at com.ocpsoft.pretty.faces.event.PrettyPhaseListener.processEvent(PrettyPhaseListener.java:214) at com.ocpsoft.pretty.faces.event.PrettyPhaseListener.afterPhase(PrettyPhaseListener.java:108) at org.apache.myfaces.lifecycle.PhaseListenerManager.informPhaseListenersAfter(PhaseListenerManager.java:111) at org.apache.myfaces.lifecycle.LifecycleImpl.executePhase(LifecycleImpl.java:185) at org.apache.myfaces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:118) at javax.faces.webapp.FacesServlet.service(FacesServlet.java:189)
Digging into the bowels of MyFaces, I discovered a class was looking for a viewId with an extension and no view-id was being set. Adding the following to the top of my execute() method solved this.
getFacesContext().getViewRoot().setViewId("/passwordHint.xhtml");
After making this change, all AppFuse's integration tests are passing and the upgrade seems complete. The only other issues I encountered were logging-related. The first is an error about Tomahawk that doesn't seem to affect anything.
Mar 5, 2011 6:44:01 AM com.sun.facelets.compiler.TagLibraryConfig loadImplicit SEVERE: Error Loading Library: jar:file:/Users/mraible/.m2/repository/org/apache/myfaces/tomahawk/tomahawk20/1.1.10/tomahawk20-1.1.10.jar!/META-INF/tomahawk.taglib.xml java.io.IOException: Error parsing [jar:file:/Users/mraible/.m2/repository/org/apache/myfaces/tomahawk/tomahawk20/1.1.10/tomahawk20-1.1.10.jar!/META-INF/tomahawk.taglib.xml]: at com.sun.facelets.compiler.TagLibraryConfig.create(TagLibraryConfig.java:410) at com.sun.facelets.compiler.TagLibraryConfig.loadImplicit(TagLibraryConfig.java:431) at com.sun.facelets.compiler.Compiler.initialize(Compiler.java:87) at com.sun.facelets.compiler.Compiler.compile(Compiler.java:104)
The second is excessive logging from MyFaces. As far as I can tell, this is because MyFaces switched to java.util.logging instead of commons logging. With all the frameworks that AppFuse leverages, I think it has all the logging frameworks in its classpath now. I was hoping to fix this by posting a message to the mailing list, but haven't received a reply yet.
[WARNING] [talledLocalContainer] Mar 5, 2011 6:50:25 AM org.apache.myfaces.config.annotation.TomcatAnnotationLifecycleProvider newInstance [WARNING] [talledLocalContainer] INFO: Creating instance of org.appfuse.webapp.action.BasePage [WARNING] [talledLocalContainer] Mar 5, 2011 6:50:25 AM org.apache.myfaces.config.annotation.TomcatAnnotationLifecycleProvider destroyInstance [WARNING] [talledLocalContainer] INFO: Destroy instance of org.appfuse.webapp.action.BasePage
After successfully upgrading AppFuse, I turned to AppFuse Light, where things were much easier.
Now that AppFuse uses JSF 2, I hope to start leveraging some of its new features. If you're yearning to get started with them today, I invite you to grab the source and start integrating them yourself.