|
A short treatise on
|
JavaServer Faces (JSF) is a user-interface (UI) component based the Java web application framework. JSF is serverbased, that is the JSF UI components and their state are represented on the server. A JSF application run in a standard web container, e.g.: Tomcat, GlassFish, Jetty, etc. JSF defines a standard life-cycle for the UI components.
These notes will one day be incorporated into a formal tutorial on writing applications using JavaServer Faces. For this reason, things get seriously random and out-right bird-brained the further you read. It's up to you when to stop paying attention.
Though these notes are extremely useful (to me) in sorting out problems in JSF, a shorter, more definitive article is my RichFaces for JSF Use.
JSF is part of the Java EE standard, formerly known as J2EE. Sun Microsystems provides a reference implementation but there are also alternatives such as Apache MyFaces.
A JSF application consists of web pages containing JSF UI components. This application requires two configuration files, web.xml and faces-config.xml".
web.xml is identical in use and composition to this file in a JavaServer Pages (JSP) application. JSF builds on JSP as a logical step beyond.
faces-config.xml defines:
A managed bean is a type of JavaBean or reusable software component, that is created and initialized with dependency injection. Dependency injection is a technique for supplying external values upon which a component or bean is dependent, usually for its initialization.
Beginning in Java EE 6, a managed bean is a Java class that...
Consider the class Person, ...
package com.etretatlogiciels.jsf.treatise; public class Person { String firstName; . . . }
Managed beans are "plain, old Java objects" (POJOs) declared in faces-config.xml. For example, you can define a Java object Person. Once you define the object in faces-config.xml you use the attributes of Person in your UI components by binding the field, say, firstName of an object of this class to a JSF input field.
JSF uses the Java Unified Expression Language, used also by JSP, to bind UI components to object attributes or methods. This "language" consists of the XML tags you will come to recognize in the JSP/JSF code.
A managed bean is specified in faces-config.xml thus:
<managed-bean> <managed-bean-name> Person </managed-bean-name> <managed-bean-class> com.etretatlogiciels.jsf.treatise.Person </managed-bean-class> <managed-bean-scope> session </managed-bean-scope> </managed-bean>
The various scopes available for managed beans are:
<managed-bean-scope> session|request|application|none </managed-bean-scope>
It is the framework within the Tomcat (etc.) container that ensures the communication of values between HTML (user/presentation) and the component (POJO).
A backing bean is not going to be part of the Controller. Although it can contain some controller functions, it's primarily part of the Model. Most of the Controller logic in JSF is in the FacesServlet and the tag implementations.
And action processors aren't controllers in the strict sense of the word. They're more of a business logic function with ties to the dispatcher.
There are three different types of dependency injection.
The benefits of dependency injection are...
Drawbacks...
In Eclipse, edit faces-config.xml and use the Managed Bean tab of the editor to create of your Java class a managed bean. Use the Initialization section to initialize the bean's properties even if they are a Map or List. In addition to the "managed-bean" specification (illustrated previously), a paragraph for "managed-property" can be added that looks like this:
<managed-property> <property-name> firstName <property-name> <property-class> com.etretatlogiciels.jsf.treatise.Person <property-class> <value> #{firstName} </value> </managed-property>
In JSF you access the values of a managed bean via value binding rather than writing code to call getters and setters.
JSP begins the concept of separating the efforts of an application between the model, the view and the controller. JSP already separates state between UI components; each component is aware of its data.
JSF introduces a separation of the functionality of a component from its presentation (or display). HTML rendering is responsible for for separate client presentation.
There are versions of JSF. JSF 1.2 is built directly atop JSP and uses JSTL tags. JSF 2.0 uses Facelets.
You can use different sets of libraries for JSF like Apache MyFaces, RealFaces, Mojarra (Sun Reference Implementation), etc. However, these are those I use.
Name | Discovered missing at |
commons-beanutils-1.8.3.jar | Server start-up |
commons-codec-1.4.jar | Application start-up (JSP launch) |
commons-collections-3.2.1.jar | (unsure, but consider it necessary) |
commons-digester-2.1.jar | Server start-up |
commons-discovery-0.4.jar | Application start-up (JSP launch) |
commons-logging-1.1.1.jar | Server start-up |
If using Apache MyFaces, here are the relevant JARs. These are responsible for actual JSF functionality (parsing, rendering, etc.).
Name |
myfaces-api-1.2.8.jar |
myfaces-impl-1.2.8.jar |
These tag libraries are imperative for
jstl-api-1.2.jar |
jstl-impl-1.2.jar |
Less relevant, here are the libraries that come with Tomcat...
annotations-api.jar |
catalina.jar |
catalina-ant.jar |
catalina-ha.jar |
catalina-tribes.jar |
el-api.jar |
jasper.jar |
jasper-el.jar |
jasper-jdt.jar |
jsp-api.jar |
servlet-api.jar |
tomcat-coyote.jar |
tomcat-i18n-es.jar |
tomcat-i18n-fr.jar |
tomcat-i18n-ja.jar |
tomcat-dbcp.jar |
...and with the JDK.
charsets.jar |
dnsns.jar |
jsse.jar |
localedata.jar |
resources.jar |
rt.jar |
sunjce_provider.jar |
sunpkcs11.jar |
These notes here are problems I've encountered then fixed as I get various JavaServer Faces programs running. Eventually, they'll find their way into my own tutorial on JSF.
If you get validation errors in your JSP file (this really isn't about JSF per se) like:
cannot find the tag library descriptor
...it probably refers to you having lost the JSTL library (add it using Build Path and also make certain it's getting deployed by checking it in Java EE Module Dependencies.
Or, you find...
Can not find the tag library descriptor for "http://java.sun.com/jsf/core"
...in...
<%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>
...right after having changed JSF libraries (say from JSF 1.2 Apache MyFaces to JSF 2.0 Apache Trinidad or JSF 2.0 Mojarra). This is fixed by going back to JSF 1.2, which distributes a complete implementation. Why this is I don't yet know.
If you're missing a tag such as given in the validation warning/error:
Unknown tag (fmt:setLocale). Unknown tag (fmt:setBundle). Unknown tag (fmt:message).
...adding this will fix it:
<%@ taglib uri='http://java.sun.com/jstl/fmt' prefix='fmt'%>
Here's an answer from the JSF forum on Java Ranch to a question on why a piece of code I wrote wasn't working. One article on the web induced me to think the syntax should be
value="#{msg.login.username}"
...but this never worked in the context in which I was trying to use it. Instead,
<f:view> <f:loadBundle basename="com.etretatlogiciels.dvdcatalog.messages" var="msg" /> <h:form> <table> <tr> <td> <h:outputText value="#{msg['login.username']}" /> </td> <td> <h:inputText id="loginname" value="#{Login.name}" /> </td> . . .
Problem: the displayed page (and Show Source) shows JSF was not rendered (or not parsed).
That can have 2 causes:
If JSF tags are not being parsed, then it simply means that the request has not been passed through the FacesServlet. That servlet is the one responsible for all that JSF stuff. You need to verify if the request URL used matches the url-pattern of the FacesServlet. Note that it is case sensitive.
This may however also happen if you opened the file directly in the builtin Eclipse browser. You shouldn't do that. You need to specify the right URL yourself in the address bar of either the builtin browser or an external browser (e.g. Internet Explorer/Firefox).
Update: one more thing, did you declare the JSF HTML taglib in the <html xmlns> attribute?
It should look like
<html ... xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html">
This is the diagnosis of the symptom, "Error configuring application listener of class org.apache.myfaces.webapp.StartupServletContextListener".
I appear to have created this error by insisting upon using Build Paths to switch the output folder from <project-name>/build/classes to <project-name>/WebContent/WEB-INF/classes. Do not do this.
My project name (as appearing below) is dvdcatalog-5 and it is certain
that this class, which comes from myfaces-impl-x.x.x.jar, is not or
should not be missing since I've got this JAR in my project, see Build
Path -> Configure Build Path... -> Libraries -> JSF (Apache
MyFaces)
.
Apr 30, 2010 8:55:24 AM org.apache.catalina.core.AprLifecycleListener init INFO: The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: C:\Users\russ\dev\downloads\jdk1.6.0_20\bin;.;C:\Windows\Sun\Java\bin;C:\Windows\system32;\ C:\Windows;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;\ C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Files\TortoiseSVN\bin;C:\Program Files (x86)\QuickTime\QTSystem\;\ C:\Program Files\Intel\DMIX;C:\Users\russ\bin;C:\Program Files (x86)\Vim\vim72;C:\Program Files (x86)\SpringSource\roo-1.0.0.RC4\bin;\ C:\Users\russ\apache-maven-2.2.1\bin;C:\Users\russ\dev\downloads\jdk1.6.0_20\bin Apr 30, 2010 8:55:24 AM org.apache.tomcat.util.digester.SetPropertiesRule begin WARNING: [SetPropertiesRule]{Server/Service/Engine/Host/Context} Setting property 'source' to 'org.eclipse.jst.jee.server:dvdcatalog-5' \ did not find a matching property. Apr 30, 2010 8:55:24 AM org.apache.coyote.http11.Http11Protocol init INFO: Initializing Coyote HTTP/1.1 on http-8080 Apr 30, 2010 8:55:24 AM org.apache.catalina.startup.Catalina load INFO: Initialization processed in 292 ms Apr 30, 2010 8:55:24 AM org.apache.catalina.core.StandardService start INFO: Starting service Catalina Apr 30, 2010 8:55:24 AM org.apache.catalina.core.StandardEngine start INFO: Starting Servlet Engine: Apache Tomcat/6.0.26 Apr 30, 2010 8:55:24 AM org.apache.catalina.core.StandardContext listenerStart SEVERE: Error configuring application listener of class org.apache.myfaces.webapp.StartupServletContextListener java.lang.ClassNotFoundException: org.apache.myfaces.webapp.StartupServletContextListener at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1516) at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1361) at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:3915) at org.apache.catalina.core.StandardContext.start(StandardContext.java:4467) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1045) at org.apache.catalina.core.StandardHost.start(StandardHost.java:785) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1045) at org.apache.catalina.core.StandardEngine.start(StandardEngine.java:443) at org.apache.catalina.core.StandardService.start(StandardService.java:519) at org.apache.catalina.core.StandardServer.start(StandardServer.java:710) at org.apache.catalina.startup.Catalina.start(Catalina.java:581) 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 org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:289) at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:414) Apr 30, 2010 8:55:24 AM org.apache.catalina.core.StandardContext listenerStart SEVERE: Skipped installing application listeners due to previous error(s) Apr 30, 2010 8:55:24 AM org.apache.catalina.core.StandardContext start SEVERE: Error listenerStart Apr 30, 2010 8:55:24 AM org.apache.catalina.core.StandardContext start SEVERE: Context [/dvdcatalog-5] startup failed due to previous errors
After this I restored build/classes to the root of my project,
respecified it in Build Path -> Configure Build Path... -> Source
-> Default output folder
and deleted
WebContent/WEB-INF/classes.
Then I refreshed, re-built, deconfigured and reconfigured the Tomcat server, cleaned, republished, etc., bounced Eclipse and more. It still would not work.
What finally did the trick was to drop Eclipse, then relaunch it with the -clean option. It has happened to me that I was obliged to do this more than once before getting it to work again.
Nonetheless, I still continue to have this problem from time to time and it got harder and harder to resolve. At last, I removed all the other projects I had to a different (new workspace) to see if that fixes it. The only project left is dvdcatalog-2. This was not a permanent fix either.
The next thing I did was get frustrated and begin to think about a different version of Apache (can't change the Dynamic Web Project's facet, Dynamic Web Module, to 2.4) or even JBoss or GlassFish (no native Eclipse support exists for GlassFish, so you have to download a special Eclipse package in addition to the GlassFish server). Finally, I deleted the server altogether from my Projet Explorer view. Then, I readded it. That worked once.
(See stack trace above more completely illustrating this error:)
SEVERE: Error configuring application listener of class org.apache.myfaces.webapp.StartupServletContextListener ...
Later, this problem persists. I note that this is the only solution that works for me and it works every time. Since beginning to do this, I have no trouble developing in JSF.
I think this is a bug in how Eclipse deploys to Tomcat, but I can't figure out how to work around or fix except following these steps.
New -> Other -> Server -> Next -> Apache ->
Tomcat v6.0 Server -> Next
.
You will have to do this every time you bounce Eclipse or change workspaces.
If you're not settled on which JSF library to use, and you've switched around, or you're using a tutorial that may use a different one, be clear that you'll get a listener missing error if you don't have the right listener referenced in web.xml. The one there must match the one in the library you're using (Apache MyFaces, RichFaces, Mojarra/Sun RI, etc.).
If the paragraph above (and the section before it) didn't help, look into this solution. Frustratingly, I got this when attempting to start Tomcat 6.
Feb 2, 2011 4:09:31 PM org.apache.catalina.core.StandardContext listenerStart SEVERE: Error configuring application listener of class org.apache.myfaces.webapp.StartupServletContextListener java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory at org.apache.myfaces.webapp.AbstractMyFacesListener.(AbstractMyFacesListener.java:36) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27) at java.lang.reflect.Constructor.newInstance(Constructor.java:513) at java.lang.Class.newInstance0(Class.java:355) at java.lang.Class.newInstance(Class.java:308) at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4079) at org.apache.catalina.core.StandardContext.start(StandardContext.java:4630) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1045) at org.apache.catalina.core.StandardHost.start(StandardHost.java:785) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1045) at org.apache.catalina.core.StandardEngine.start(StandardEngine.java:445) at org.apache.catalina.core.StandardService.start(StandardService.java:519) at org.apache.catalina.core.StandardServer.start(StandardServer.java:710) at org.apache.catalina.startup.Catalina.start(Catalina.java:581) 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 org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:289) at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:414) Caused by: java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1645) at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1491) ... 22 more Feb 2, 2011 4:09:31 PM org.apache.catalina.core.StandardContext listenerStart SEVERE: Skipped installing application listeners due to previous error(s) Feb 2, 2011 4:09:31 PM org.apache.catalina.core.StandardContext start SEVERE: Error listenerStart Feb 2, 2011 4:09:31 PM org.apache.catalina.core.StandardContext start SEVERE: Context [/de.vogella.jsf.starter] startup failed due to previous errors
This despite that I'm using the following JARs:
...and, clearly, this class is not missing!
$ jar -tvf myfaces-impl-1.2.8.jar | grep StartupServletContextListener 5747 Mon Nov 16 22:34:04 MST 2009 org/apache/myfaces/webapp/StartupServletContextListener.class
Don't keep looking for the Listener! That's nothing to do with it. It's the logging stuff that's missing. What is missing here is the Apache Commons libraries, in particular, commons-logging-1.1.1.jar.
If there's a javax.faces.DEFAULT_SUFFIX in web.xml of, e.g.: .jsp or .jsf, and a <servlet-mapping ... url-pattern> that matches, this will cause infinite recursion:
SEVERE: Servlet.service() for servlet Faces Servlet threw exception java.lang.StackOverflowError at javax.xervlet.ServletResponseWrapper.setContentType(ServletResponseWrapper.java:130)
Find some here.
These are just warnings that nothing's been set in web.xml and a default
These are just warnings that nothing's been set in web.xml and a default is being taken.
. . . INFO - Checking for plugins:org.apache.myfaces.FACES_INIT_PLUGINS INFO - No context init parameter 'org.apache.myfaces.RENDER_CLEAR_JAVASCRIPT_FOR_BUTTON' found, using default value false INFO - No context init parameter 'org.apache.myfaces.SAVE_FORM_SUBMIT_LINK_IE' found, using default value false INFO - No context init parameter 'org.apache.myfaces.READONLY_AS_DISABLED_FOR_SELECTS' found, using default value true INFO - No context init parameter 'org.apache.myfaces.RENDER_VIEWSTATE_ID' found, using default value true INFO - No context init parameter 'org.apache.myfaces.STRICT_XHTML_LINKS' found, using default value true INFO - No context init parameter 'org.apache.myfaces.CONFIG_REFRESH_PERIOD' found, using default value 2 INFO - No context init parameter 'org.apache.myfaces.VIEWSTATE_JAVASCRIPT' found, using default value false . . .
Tomahawk is built upon MyFaces, but it isn't mandatory, crucial or important to include. This just says it looked for it, but it isn't there.
. . . INFO - Tomahawk jar not available. Autoscrolling, DetectJavascript, AddResourceClass and CheckExtensionsFilter are disabled now. INFO - Starting up Tomahawk on the MyFaces-JSF-Implementation . . .
More informative remarks. Surprisingly, it says it's getting myfaces
from WEB-INF/lib. That's not where we installed this library, but it's
the net effect of setting it up using a
Java Build Path -> Libraries -> User Library
plus setting Java EE Module Dependencies.
. . . INFO - Reading standard config META-INF/standard-faces-config.xml INFO - Reading config /WEB-INF/faces-config.xml INFO - Starting up MyFaces-package : myfaces-api in version : 1.2.8 from path : \ file:/C:/Users/russ/dev/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/dvdcatalog-2/WEB-INF/lib/myfaces-api-1.2.8.jar INFO - Starting up MyFaces-package : myfaces-impl in version : 1.2.8 from path : \ file:/C:/Users/russ/dev/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/dvdcatalog-2/WEB-INF/lib/myfaces-impl-1.2.8.jar . . .
Again, just noting that these are missing, but it shouldn't matter...
. . . INFO - MyFaces-package : tomahawk not found. INFO - MyFaces-package : tomahawk12 not found. INFO - MyFaces-package : tomahawk-sandbox not found. INFO - MyFaces-package : tomahawk-sandbox12 not found. INFO - MyFaces-package : tomahawk-sandbox15 not found. INFO - MyFaces-package : myfaces-orchestra-core not found. INFO - MyFaces-package : myfaces-orchestra-core12 not found. INFO - MyFaces-package : trinidad-api not found. INFO - MyFaces-package : trinidad-impl not found. INFO - MyFaces-package : tobago not found. INFO - MyFaces-package : commons-el not found. INFO - MyFaces-package : jsp-api not found. . . .
This is probably due to a "java.lang.StackOverflowError".
INFO: Server startup in 984 ms May 3, 2010 10:08:45 AM org.apache.catalina.core.ApplicationDispatcher invoke SEVERE: Servlet.service() for servlet faces threw exception java.lang.StackOverflowError at org.apache.catalina.core.ApplicationHttpRequest.getAttributeNames(ApplicationHttpRequest.java:243) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.(ApplicationHttpRequest.java:905) at org.apache.catalina.core.ApplicationHttpRequest.getAttributeNames(ApplicationHttpRequest.java:243) . . .
This arises from having duplicate <servlet> entries in web.xml, such as:
. . .faces *.jsf . . . faces *.jsp
You cannot configure faces for .jsp and .jsf. I've found that not defining .jsp that way is the solution. For a Faces application, it's not necessary even though you might be led to think it is by reason of the presence of .jsp files (and not files extended with .jsf).
This is a purely Eclipse-caused error. It usually indicates a different project than the one you're running and that this project has been closed.
. . . May 3, 2010 11:08:17 AM org.apache.catalina.core.StandardEngine start INFO: Starting Servlet Engine: Apache Tomcat/6.0.26 May 3, 2010 11:08:17 AM org.apache.catalina.core.StandardContext resourcesStart SEVERE: Error starting static Resources java.lang.IllegalArgumentException: Document base \ C:\Users\russ\dev\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\dvdcatalog-3 does not exist or is not a readable directory at org.apache.naming.resources.FileDirContext.setDocBase(FileDirContext.java:142) at org.apache.catalina.core.StandardContext.resourcesStart(StandardContext.java:4086) at org.apache.catalina.core.StandardContext.start(StandardContext.java:4255) . . .
Be careful where you create your stylesheets and images. The resulting HTML page cannot access anything under WEB-INF, so if you created subdirectories like images and styles or otherwise put files under WEB-INF you're trying to access from your JSP file, they will not be found. The symptoms are a) missing images and b) wrong font, styles, etc.
If you're missing the alt attribute on an image, you'll get this warning:
WARN - ALT attribute is missing for : j_id_jsp_1158619322_2
Add it thus:
<h:graphicImage value="#{msg['dvdcatalog.logo']}" alt="[dvdcatalog-logo.png]" />
After typing in username and password, you get this:
. . . INFO: Server startup in 1000 ms ERROR - An exception occurred javax.faces.FacesException: javax.crypto.BadPaddingException: Given final block not properly padded at org.apache.myfaces.shared_impl.util.StateUtils.symmetric(StateUtils.java:474) at org.apache.myfaces.shared_impl.util.StateUtils.symmetric(StateUtils.java:512) . . .
I added this to web.xml. It opens a security hole, but it appears to be a bug in MyFaces.
<context-param> <description> This is my attempt to get rid of the javax.faces.FacesException: javax.crypto.BadPaddingException: Given final block not properly padded problem. </description> <param-name>org.apache.myfaces.USE_ENCRYPTION</param-name> <param-value>false </context-param>
After typing in username and password, you get this:
. . . INFO: Server startup in 1009 ms ERROR - An exception occurred javax.faces.FacesException: java.io.StreamCorruptedException: invalid stream header: 70DB8AB1 at org.apache.myfaces.shared_impl.util.StateUtils.getAsObject(StateUtils.java:374) at org.apache.myfaces.shared_impl.util.StateUtils.reconstruct(StateUtils.java:264) . . .
I found no solution to this. I went back to a working JSF project (not the same one) to see how it worked. It magically had this same problem. So, I bounced Eclipse passing the -clean option. That cleared both the old, good project and my new one. Incidentally, the old project didn't have the web.xml work-around for the "final block not properly padded" problem, so that was a red herring too.
Sometimes, when there are no console start-up errors and everything should work out alright, you get an error on the web page.
Usually, 404 errors are irrecoverable. Look to solve them just as for the ones discussed here that I've illustrated on the Eclipse console.
However, when they're a JSP/JSF error, they will tell you either a) about a mistake you've made in your HTML/JSP/JSF source or in your bean, or b) they are a red herring left over from an earlier error you did your best to clear.
Let's say you ran and the page complained about not finding the Standard Tag Library for JavaServer Pages (JSTL); you need both the JARs from Sun to satisfy the missing symbols. You check Build Path and discover that your project isn't including them and you rectify this and attempt to run again only to see that the same error is reported.
You might even remove and set up the Server again, or in frustration perform some other heroic action only to find the error is still there. Sometimes, usually in cases like this one, the solution is merely to click to refresh the page.
When Eclipse deploys to the local Tomcat server in the workspace, it creates a JAR in your workspace on the path:
.metadata/.plugins/org.eclipse.wst.server.core/tmp?/wtpwebapps/project-name/WEB-INF/lib
...along with all the other JARs specified for publication via Properties
-> Java EE Module Dependencies
which should match more or less what
you have in Build Path -> Configure Build Path -> Libraries
.
This would be the JAR being used by Tomcat at run-time and in which it would expect to find all needed classes. Normal publishing would copy it (them) once and not re-copy unless a "full" publish occurred. Corruption of this JAR is the only simple explanation of why a class-not-found problem "sticks" once it occurs.
Note that a ClassNotFoundException thrown from line 1516 of WebappClassLoader of Tomcat 6.0.26 is the "I give up" ClassNotFoundException. It's conceivable that the internal attempt to load the class, which should have succeeded, fails due to some problem other than the class not being found. The "search" to load the class could continue with the "real" problem going unreported. logging.properties can be configured to log that internal load failure.
My very first attempt at this "advanced" JSF skill. Imagine a section of JSF code such as:
<h:selectOneMenu id="shippingOption" required="true" value="#{cashier.shippingOption}"> <f:selectItem itemValue="2" itemLabel="#{bundle.QuickShip}"/> <f:selectItem itemValue="5" itemLabel="#{bundle.NormalShip}"/> <f:selectItem itemValue="7" itemLabel="#{bundle.SaverShip}"/> </h:selectOneMenu>
Here is the property, in CashierBean.java corresponding to this tag:
protected String shippingOption = "2"; public void setShippingOption( String shippingOption ) { this.shippingOption = shippingOption; } public String getShippingOption() { return this.shippingOption; }
That was one example I found by Googling, and the only one (believe it or not) that revealed its backing-bean code. The others just gave the JSF and assumed the bean code was simple, clear and unworthy to be featured. Of course, they assumed I already knew how this selectOneListbox construct worked while I needed just a bit more help piercing the veil.
I resorted there especially in an attempt to resolve an Eclipse JSP/JSF
validation error in view.jsp, "JSF attribute expects settable value but
expression is not settable". I couldn't get what goes into value just
right. You see the shippingOption in
value="#{cashier.shippingOption}"
is crucial.
It's not just the initial value to display in the drop-down menu when it hits the browser, but also must be able to gather that value if changed. This input/output relationship in JSF is wired delicately on the backing bean's getters and setters, in this case, getShippingOption() and setShippingOption().
In my own code, selecting different types of query filters went like this:
<h:selectOneListbox size="1" <%-- (show only one option at a time) --%> title="#{msg['view.filter_prompt']}" value="#{queryBean.filterMethod}"> <f:selectItems value="#{queryBean.filterMethodsList}"/> </h:selectOneListbox>
This is, I believe, correct. My backing bean is where I had the trouble and was either crashing or, more frequently, merely in some infinite, "I'll never display anything" loop. Here's how it started out the first time it didn't crash or loop out:
private String filterMethod = null; private static LinkedHashMap< Integer, String > filterMethods = null; static { redoMessages(); // (default initialization of messages) } public static void redoMessages() // (ChangeLocaleBean.xxAction() calls this when the locale has changed) { filterMethods = new LinkedHashMap< Integer, String >(); filterMethods.put( 1, getGenericMessage( "view.filter.all" ) ); filterMethods.put( 2, getGenericMessage( "view.filter.year" ) ); filterMethods.put( 3, getGenericMessage( "view.filter.rating" ) ); filterMethods.put( 4, getGenericMessage( "view.filter.genre" ) ); filterMethods.put( 5, getGenericMessage( "view.filter.language" ) ); } // this is for handling the <h:selectOneListbox value=...> construct public void setFilterMethod( String newMethod ) { this.filterMethod = newMethod; } public String getFilterMethod() { return this.filterMethod; } // this is for building the <f:selectItems ...> construct public Map< Integer, String > getFilterMethodsList() { return filterMethods; }
This gave me:
Of course, I want very different things in the filter drop-down list, so I
reverse the order thinking I'll get my strings instead of the numbers. On my
second try, which was to change the type from
< Integer, String >
to
< String, Integer >
—how I wanted it in the first
place, I got a crash instead:
I think the important part of the error is "value is no String" which is why I
had changed it to the "unlikely" order in the first place. Another key part of
the error is "HtmlSelectOneListbox" which tells me to look at my
<h:selectOneListbox ...>
construct instead of the
<f:selectItems ...>
construct to find the problem.
But, what's the solution?
On my third try, I changed
< String, Integer >
to
< String, String >
and then adjusted other code:
. . . filterMethods = new LinkedHashMap< String, String >(); filterMethods.put( getGenericMessage( "view.filter.all" ), "1" ); filterMethods.put( getGenericMessage( "view.filter.year" ), "2" ); filterMethods.put( getGenericMessage( "view.filter.rating" ), "3" ); filterMethods.put( getGenericMessage( "view.filter.genre" ), "4" ); filterMethods.put( getGenericMessage( "view.filter.language" ), "5" ); . . .
...and I got the following, which is closer, missing only the localization:
I went to add the ratings drop-down. I copied the filter method work and then ran only to get:
The reason I'm reproducing this here is out of honesty and to expose another sort of error, as stupid as it might be. In this instance, I failed to add the first line of code here and there was no list ready for items to be put to.
ratings = new LinkedHashMap< String, String >(); ratings.put( getGenericMessage( "view.rating.NR" ), "1" ); ratings.put( getGenericMessage( "view.rating.G" ), "2" ); ratings.put( getGenericMessage( "view.rating.PG" ), "3" ); ratings.put( getGenericMessage( "view.rating.PG_13" ), "4" ); ratings.put( getGenericMessage( "view.rating.NC_17" ), "5" ); ratings.put( getGenericMessage( "view.rating.R" ), "6" );
The line number in the error doesn't correspond to where the new JSF code was put for the rating drop-down, but here. Line 27 isn't too enlightening, but think about it: changing the locale is what reaches the Java code above. (Or, I could have refreshed; it's the same.)
Making large-scale changes such as these may not even appear in the browser when you run the JSP. When this happens, do the following, including the last one when it just really persists:
Ultimately, coolness will begin to prevail:
Onward and upward!
Now switching our year list from selectOneListbox to selectManyListbox, we crash with this error, doubtless linked to what we need to do beyond how the other kind of list is done.
This is because we're not passing the right sort of animal to value; it's going to be different from what we pass to selectOneListbox. Please study the code below for the differences between selectOneListbox and selectManyListbox.
Then see the result which has been heavily reformatted to make room for everything. Also, the years field became something that will have to be parsed (and therefore some kind of high-level SQL statement), and the "many" list boxes cannot (apparently) be scrollable. Or can they? I need to investigate.
public class QueryBean { private static Logger log = Logger.getLogger( MessageBean.class ); private static Locale locale = null; private static String bundleName = null; private String filterMethod = null; private static LinkedHashMap< String, String > filterMethods = null; private String[] ratings = null; private static LinkedHashMap< String, String > ratingsItems = null; private String[] genres = null; private static LinkedHashMap< String, String > genreItems = null; private String[] languages = null; private static LinkedHashMap< String, String > languageItems = null; private String sortedBy = null; private static LinkedHashMap< String, String > sortings = null; private String years = null; static { redoMessages(); } private static final void getFacesContext() { FacesContext context = FacesContext.getCurrentInstance(); if( context != null ) { locale = context.getViewRoot().getLocale(); bundleName = context.getApplication().getMessageBundle(); } } /** * Call this when the locale has changed. */ public static void redoMessages() { getFacesContext(); if( locale == null || bundleName == null ) return; log.info( "Redoing messages..." ); filterMethods = new LinkedHashMap< String, String >(); filterMethods.put( getGenericMessage( "view.filter.all" ), "1" ); filterMethods.put( getGenericMessage( "view.filter.year" ), "2" ); filterMethods.put( getGenericMessage( "view.filter.rating" ), "3" ); filterMethods.put( getGenericMessage( "view.filter.genre" ), "4" ); filterMethods.put( getGenericMessage( "view.filter.language" ), "5" ); ratingsItems = new LinkedHashMap< String, String >(); ratingsItems.put( getGenericMessage( "view.rating.NR" ), "1" ); ratingsItems.put( getGenericMessage( "view.rating.G" ), "2" ); ratingsItems.put( getGenericMessage( "view.rating.PG" ), "3" ); ratingsItems.put( getGenericMessage( "view.rating.PG_13" ), "4" ); ratingsItems.put( getGenericMessage( "view.rating.NC_17" ), "5" ); ratingsItems.put( getGenericMessage( "view.rating.R" ), "6" ); genreItems = new LinkedHashMap< String, String >(); genreItems.put( getGenericMessage( "view.genre.action" ), "1" ); genreItems.put( getGenericMessage( "view.genre.adventure" ), "2" ); genreItems.put( getGenericMessage( "view.genre.allegory" ), "3" ); genreItems.put( getGenericMessage( "view.genre.cartoon" ), "4" ); genreItems.put( getGenericMessage( "view.genre.chick" ), "5" ); genreItems.put( getGenericMessage( "view.genre.comedy" ), "6" ); genreItems.put( getGenericMessage( "view.genre.drama" ), "7" ); genreItems.put( getGenericMessage( "view.genre.fantasy" ), "8" ); genreItems.put( getGenericMessage( "view.genre.history" ), "9" ); genreItems.put( getGenericMessage( "view.genre.military" ), "10" ); genreItems.put( getGenericMessage( "view.genre.music" ), "11" ); genreItems.put( getGenericMessage( "view.genre.musical" ), "12" ); genreItems.put( getGenericMessage( "view.genre.mystery" ), "13" ); genreItems.put( getGenericMessage( "view.genre.politics" ), "14" ); genreItems.put( getGenericMessage( "view.genre.religious" ), "15" ); genreItems.put( getGenericMessage( "view.genre.science" ), "16" ); genreItems.put( getGenericMessage( "view.genre.suspense" ), "17" ); genreItems.put( getGenericMessage( "view.genre.thriller" ), "18" ); languageItems = new LinkedHashMap< String, String >(); languageItems.put( getGenericMessage( "view.language.chinese" ), "zh" ); languageItems.put( getGenericMessage( "view.language.finnish" ), "su" ); languageItems.put( getGenericMessage( "view.language.french" ), "fr" ); languageItems.put( getGenericMessage( "view.language.german" ), "de" ); languageItems.put( getGenericMessage( "view.language.greek" ), "el" ); languageItems.put( getGenericMessage( "view.language.italian" ), "it" ); languageItems.put( getGenericMessage( "view.language.japanese" ), "ja" ); languageItems.put( getGenericMessage( "view.language.korean" ), "ko" ); languageItems.put( getGenericMessage( "view.language.portuguese" ),"pt" ); languageItems.put( getGenericMessage( "view.language.russian" ), "10" ); languageItems.put( getGenericMessage( "view.language.spanish" ), "es" ); languageItems.put( getGenericMessage( "view.language.thai" ), "th" ); sortings = new LinkedHashMap< String, String >(); sortings.put( getGenericMessage( "view.sort.title" ), "1" ); sortings.put( getGenericMessage( "view.sort.year" ), "2" ); sortings.put( getGenericMessage( "view.sort.rating" ),"3" ); } // this is for handling the <h:selectOneListbox value=...> construct for filter methods... public void setFilterMethod( String newValue ) { this.filterMethod = newValue; } public String getFilterMethod() { return this.filterMethod; } // this is for building the <f:selectItems ...> construct public Map< String, String > getFilterMethodsList() { return filterMethods; } // this is for handling the <h:selectManyListbox value=...> construct for ratings... public void setRatings( String[] newValue ) { this.ratings = newValue; } public String[] getRatings() { return this.ratings; } // this is for building the <f:selectItems ...> construct public Map< String, String > getRatingsItems() { return ratingsItems; } // and for genre (etc.)... public void setGenres( String[] newValue ) { this.genres = newValue; } public String[] getGenres() { return this.genres; } public Map< String, String > getGenreItems() { return genreItems; } // and for languages... public void setLanguages( String[] newValue ) { this.languages = newValue; } public String[] getLanguages() { return this.languages; } public Map< String, String > getLanguagesItems() { return languageItems; } // and for sortings... public void setSortedBy( String newValue ) { this.sortedBy = newValue; } public String getSortedBy() { return this.sortedBy; } public Map< String, String > getSortingsList() { return sortings; } public void setYears( String newValue ) { this.years = newValue; } public String getYears() { return this.years; } private static final String getGenericMessage( String messageKey ) { String text = MessageUtils.getMessageResourceString( bundleName, messageKey, null, locale ); if( text == null ) { text = "No message bundle or no property \"dvdcatalog.title\""; log.info( text ); } return text; } }
...and the JSF code illustrates outputText, inputText, selectOneListbox, selectManyListbox and selectItems:
.gutter { width: 20px; } .query-prompt { color: maroon; font-size: small; font-weight: bold; text-align: right; }
. . . <center> <h2> <h:outputText value="#{msg['dvdcatalog.title']}" /> </h2> <div class="textbox"> <p> <h:outputText value="#{msg['view.last']}" /> <h:outputText value=" #{statisticsBean.lastUpdate}" /> <br /> <h:outputText value="#{msg['view.total']}" /> <h:outputText value=" #{statisticsBean.totalTitles}" /> <br /> <h:outputText value="#{msg['view.foreign']}" /> <h:outputText value=" #{statisticsBean.totalForeignTitles}" /> </p> <p class="notes"> <h:outputText value="#{msg['view.legend']}" /> <h:outputText value="#{msg['view.notes_1']}" /> <h:outputText value="#{msg['view.notes_2']}" /> </p> <table> <tr><td> <table> <tr><td class="query-prompt"> <%-- Filter this list by: --%> <h:outputText value="#{msg['view.filter_prompt']}" /> </td> <td> <h:selectOneListbox size="1" title="#{msg['view.filter_prompt']}" value="#{queryBean.filterMethod}"> <f:selectItems value="#{queryBean.filterMethodsList}"/> </h:selectOneListbox> </td> <td class="gutter"> </td> <td class="query-prompt"> <%-- Sort this list by: --%> <h:outputText value="#{msg['view.sort_prompt']}" /> </td> <td> <h:selectOneListbox size="1" title="#{msg['view.sort_prompt']}" value="#{queryBean.sortedBy}"> <f:selectItems value="#{queryBean.sortingsList}"/> </h:selectOneListbox> </td> </tr> </table> </td> </tr> <tr><td> </td></tr> <tr><td valign="top"> <table> <tr><td valign="top"> <table> <tr><td class="query-prompt" valign="top"> <%-- Rating: --%> <h:outputText value="#{msg['view.rating_prompt']}" /> </td> <td valign="top"> <h:selectManyListbox title="#{msg['view.rating_prompt']}" value="#{queryBean.ratings}"> <f:selectItems value="#{queryBean.ratingsItems}"/> </h:selectManyListbox> </td> </tr> <tr><td> </td> </tr> <tr> <td class="query-prompt" valign="top"> <%-- Years: --%> <h:outputText value="#{msg['view.year_prompt']}" /> </td> <td> <h:inputText value="#{queryBean.years}" /> </td> </tr> </table> </td> <td class="gutter"> </td> <td class="query-prompt" valign="top"> <%-- Genre: --%> <h:outputText value="#{msg['view.genre_prompt']}" /> </td> <td valign="top"> <h:selectManyListbox title="#{msg['view.genre_prompt']}" value="#{queryBean.genres}"> <f:selectItems value="#{queryBean.genreItems}"/> </h:selectManyListbox> </td> <td class="gutter"> </td> <td class="query-prompt" valign="top"> <%-- Language: --%> <h:outputText value="#{msg['view.language_prompt']}" /> </td> <td valign="top"> <h:selectManyListbox title="#{msg['view.language_prompt']}" value="#{queryBean.languages}"> <f:selectItems value="#{queryBean.languagesItems}"/> </h:selectManyListbox> </td> </tr> </table> </td> </tr> <tr><td colspan="3" /> </tr> <tr> <td colspan="3" align="center"> <table> <tr><td>[button]</td> <td class="gutter"></td> <td>[button]</td> </tr> </table> </td> </tr> </table> </div> </center> . . .
It's easy to get lost in all the attributes passed to the likes of
Here's how I think of it: Consider what the construct does. If it's just to force some string to the page when renderedi or it's the title of a control (button or link), then the value attribute is likely going to be an expression that evaluates to a string, number or graphic. It probably comes out of messages.properties or similar file.
<h:outputLabel value="#{msg.username}" />
This includes command links and buttons because of the text item associated with them.
On the other hand, if the tag is looking for dynamically created values (at run-time), it likely indicates a bean property (method). For example, here, the first value is a list of supported (or considered) languages that could (or may not necessarily) fluctuate at run-time.
The second value is an array of the same languages, except as items in a selection control and it is composed for being set (as state) in the bean. It is the "result" of the user's answering the form. So, when done, it's a list of zero, one or more languages. From the bean, the code...
private String[] languages = { "French", "Latin", "Greek", "German" }; private static LinkedHashMap< String, String > languageItems = null; . . . languageItems = new LinkedHashMap< String, String >(); languageItems.put( getGenericMessage( "view.language.french" ), "fr" ); languageItems.put( getGenericMessage( "view.language.french" ), "la" ); languageItems.put( getGenericMessage( "view.language.french" ), "el" ); languageItems.put( getGenericMessage( "view.language.french" ), "de" ); . . . public String[] getLanguages() { return this.languages; } public Map< String, String > getLanguageItems() { return languageItems; }
...underlies the JSF constructs. languages may be initialized statically at compile-time or at run-time or even dynamically at run-time.
<h:selectManyListbox title="#{msg['view.language_prompt']}" value="#{queryBean.languages}"> <f:selectItems value="#{queryBean.languageItems}" /> </h:selectManyListbox>
I need to consume SelectItem instead of String[]. See discussion at "selectManyListbox value attribute". This may also affect selectOneListbox.
In a selectOneListbox, value is a little easier. First, it's the same thing as as for the selectManyListbox, but the second one is a single thing and not an array since one and only one must be chosen.
<h:selectOneListbox size="1" title="#{msg['view.filter_prompt']}" value="#{queryBean.filter}"> <f:selectItems value="#{queryBean.filterList}" /> </h:selectOneListbox>
The action attribute is always going to be a bean method that returns a String.
<h:commandButton value="#{msg['view.reset_button']}" action="#{queryBean.clear}" type="reset" />
The "action" method is going to return a String. This is all important since it's this string that determines JSF navigation as described in the from-outcome element of the navigation-case of a navigation-rule in faces-config.xml.
public String clear() { log.info( "Resetting query parameters..." ); this.filter = null; this.languages = null; return "reset"; }
It's important to note that the type attribute of an <h:commandButton> can never be anything but "submit" or "reset". See the Tag commandButton documentation.
Set up navigation to look like this:
You can do this visually using the faces-config.xml Navigation Rule editor or simply add these lines to faces-config.xml by hand:
login /login.jsp succeeded /validlogin.jsp login /login.jsp failed /invalidlogin.jsp login /login.jsp power /powerlogin.jsp validlogin /validlogin.jsp query /query.jsp powerlogin /powerlogin.jsp query /query.jsp query /query.jsp view /view.jsp view /view.jsp query /query.jsp
Note: The "power-login" stuff is for a putative power user that will have the ability to add, delete or modify entries in the database. This work won't appear in this tutorial, but is already stubbed in and I don't want to remove it.
Here's the log-in page. You can control the user interface lnaguage at this point (and again from the query page).
And here's the welcome page. It for me, a power user and signals this by reprising a line from Hunt for Red October.
The ordinary welcome page appears thus—without the power statement about being authorized to launch missiles.
If we had failed, if we had entered user "snagglepuss" with password "the lion", we'd have seen an error message on the login page rather than invalidlogin.jsp getting used. We may take out this additional, unused JSP.
Finally, clicking with a valid log-in on the Configure query button gets us to our more or less finished and working query page. We just have to write and debug our view page and wire up the Hibernate guts.
It appears that the following warning cannot be fixed. There are three
occurrences in query.jsp; this one is flagged on the construct
#{queryBean.ratings}
.
<h:selectManyListbox title="#{msg['view.rating_prompt']}" value="#{queryBean.ratings}"> Cannot coerce type java.lang.String[] to java.lang.String, java.lang.Short, java.lang.Character, java.lang.Boolean, java.lang.Double, java.lang.Byte, java.lang.Long, java.lang.Float, java.lang.Integer
See Tag Library Documentation for entry point to both h and f tags.
A good reference here. The Javadoc is at h Tag dataTable.
...rowClass="oddRow,evenRow">...where styles might be defined thus:
.evenRow { background-image: url('images/bg1.jpg'); } .oddRow { background-image: url('images/bg2.jpg'); }Alternates every other row according to "odd-row" / "even-row" attributes. In other words, ...
These row styles are applied to each row in the table in turn. If the list has two elements ("odd, even"), the first style class in the list is applied to the first row, the second to the second row, the first to the third row, the second to the fourth row, etc. If the list has three elements, then the alternance is based on three. Etc.
Use of attribute var
var...
- is not required
- must evaluate to String
- the name of a request-scope attribute under which the model data for the row selected by the current value of the rowIndex property will be exposed.
Use it as a "cookie" according to the following pseudocode. Imagine a Java class, TupleBean, that returns tuples by name and value. The utility of prefacing the cookie name with an underscore, as suggested by one tutorial I read, is to avoid it confusing the reader with other entities (classes, methods, bean names, etc.). TupleBean furnishes the getters getName() and getValue().
<h:dataTable var="_tuple" value="#{tupleBean.all}"> <h:column> <f:facet name="header"> <h:outputText value="Name" /> </f:facet> <h:outputText value="#{_tuple.name}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Value" /> </f:facet> <h:outputText value="#{_tuple.value}" /> </h:column> </h:dataTable>Note: if the table as displayed is empty, it's only because the getAll() method (from TupleBean) failed to produce any rows.
Sample rendering of an <h:dataTable>
Consider the following <h:dataTable> construct from a JSF file. This should help you visualize how what's coded in JSF and CSS is used to generate the page dynamically.
<h:dataTable value="#{dvdTitle.all}" var="_title" styleClass="dvdStyles" headerClass="dvdHeader" columnClasses="dvdId,dvdBluray,dvdTitle,dvdYear,dvdMinutes,dvdRating,dvdGenre,dvdLanguage,dvdUrl,dvdStars" rowClasses="oddRow,evenRow"> <h:column> <f:facet name="header"> <h:outputText value="#{msg['view.id']}" /> </f:facet> <h:outputText value="#{_title.id}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msg['view.bluray']}" /> </f:facet> <h:outputText value="#{_title.getBluray}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msg['view.title']}" /> </f:facet> <h:outputText value="#{_title.title}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msg['view.year']}" /> </f:facet> <h:outputText value="#{_title.year}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msg['view.minutes']}" /> </f:facet> <h:outputText value="#{_title.minutes}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msg['view.rating']}" /> </f:facet> <h:outputText value="#{_title.rating}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msg['view.genre']}" /> </f:facet> <h:outputText value="#{_title.genre}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msg['view.language']}" /> </f:facet> <h:outputText value="#{_title.language}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msg['view.url']}" /> </f:facet> <h:outputText value="#{_title.url}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msg['view.stars']}" /> </f:facet> <h:outputText value="#{_title.stars}" /> </h:column> </h:dataTable>Notice that each element (data cell, header, etc. has its own style setting). The <f:facet> tag is either "header" or "footer" ("caption", etc.) and thus the dvdHeader style class is associated with our usage (we have no footer material here). This is what's generated for display in the browser.
<table class="dvdStyles"> <thead> <tr> <th class="dvdHeader" scope="col"> id </th> <th class="dvdHeader" scope="col"> Blu-ray? </th> <th class="dvdHeader" scope="col"> Title </th> <th class="dvdHeader" scope="col"> Year </th> <th class="dvdHeader" scope="col"> Length </th> <th class="dvdHeader" scope="col"> Rating </th> <th class="dvdHeader" scope="col"> Genres </th> <th class="dvdHeader" scope="col"> Languages </th> <th class="dvdHeader" scope="col"> Link </th> <th class="dvdHeader" scope="col"> Stars </th> </tr> </thead> <tbody id="j_id_jsp_458090977_1:j_id_jsp_458090977_7:tbody_element"> <tr class="oddRow"> <td class="dvdId"> 1 </td> <td class="dvdBluray"> x </td> <td class="dvdTitle"> Lord of the Rings: Fellowship of the Ring</td> <td class="dvdYear"> 2001 </td> <td class="dvdMinutes"> 180 </td> <td class="dvdRating"> PG-13 </td> <td class="dvdGenre"> Adventure </td> <td class="dvdLanguage">Spanish </td> <td class="dvdUrl"> www.lordoftherings.net </td> <td class="dvdStars"> Elijah Wood, Sean Austin, Dominique Monahan, Billy Boyd, Viggo Mortensen, Sean Bean, Orlando Gibbons, John Rhys-Davies, Ian McKellen</td> </tr> <tr class="evenRow"> <td class="dvdId"> 2 </td> <td class="dvdBluray"> x </td> <td class="dvdTitle"> Lord of the Rings: The Two Towers</td> <td class="dvdYear"> 2002 </td> <td class="dvdMinutes"> 180 </td> <td class="dvdRating"> PG-13 </td> <td class="dvdGenre"> Adventure </td> <td class="dvdLanguage">Spanish </td> <td class="dvdUrl"> www.lordoftherings.net </td> <td class="dvdStars"> Elijah Wood, Sean Austin, Dominique Monahan, Billy Boyd, Viggo Mortensen, Sean Bean, Orlando Gibbons, John Rhys-Davies, Ian McKellen</td> </tr> <tr> . . . </tr> </tbody> </table>And here is the relevant portion of the cascading-styles sheet:
/* DVD titles list... */ .dvdStyles /* class for the <table> tag rendering an <h:dataTable> */ { border: thin solid black; } .dvdHeader /* class for <th> tags in the <thead> rendering an <h:dataTable> */ { text-align: left; vertical-align: text-top; font-style: italic; color: maroon; } .dvdId { width: 10px; text-align: left; } .dvdTitle { width: 300px; text-align: left; } .dvdBluray { width: 80px; } .dvdYear { width: 20px; } .dvdMinutes { width: 20px; } .dvdRating { width: 20px; } .dvdGenre { width: 80px; } .dvdLanguage{ width: 80px; } .dvdUrl { width: 80px; } .dvdStars { width: 80px; } .evenRow { background-image: url('bg1.jpg'); } .oddRow { background-image: url('bg2.jpg'); }Appendix: view.jsp
Here's the view page at one point with four titles in the database, JSF and CSS work done closer to what I hope to end up with. Notice that the "Movie site" links are actual links and if you click on them, they take you to the movie site (if the link isn't stale or bogus).
Appendix: web.xml
This is the application's web.xml that works both for the Tomcat Eclipse uses and the "real" one installed. Despite the *.jsf entry below, we have only .jsp files; this works anyway and, in fact, does not if we change the entry to specify .jsp.
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <display-name>dvdcatalog</display-name> <welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file> <welcome-file>default.html</welcome-file> <welcome-file>default.htm</welcome-file> <welcome-file>default.jsp</welcome-file> </welcome-file-list> <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>/faces/*</url-pattern> </servlet-mapping> <context-param> <param-name>javax.servlet.jsp.jstl.fmt.localizationContext</param-name> <param-value>resources.application</param-value> </context-param> <context-param> <param-name>javax.faces.STATE_SAVING_METHOD</param-name> <param-value>client</param-value> </context-param> <context-param> <param-name>org.apache.myfaces.ALLOW_JAVASCRIPT</param-name> <param-value>true</param-value> </context-param> <context-param> <param-name>org.apache.myfaces.PRETTY_HTML</param-name> <param-value>true</param-value> </context-param> <context-param> <param-name>org.apache.myfaces.DETECT_JAVASCRIPT</param-name> <param-value>false</param-value> </context-param> <context-param> <param-name>org.apache.myfaces.AUTO_SCROLL</param-name> <param-value>true</param-value> </context-param> <servlet> <servlet-name>faces</servlet-name> <servlet-class>org.apache.myfaces.webapp.MyFacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>faces</servlet-name> <url-pattern>*.jsf</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>faces</servlet-name> <url-pattern>*.faces</url-pattern> </servlet-mapping> <listener> <listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class> </listener> <context-param> <param-name>org.apache.myfaces.USE_ENCRYPTION</param-name> <param-value>false</param-value> </context-param> </web-app>Appendix: Where to find deployment?
Eclipse deploys to "its" Tomcat on a peculiar path. It's sometimes useful to go looking for this in diagnosing problems such as working on a project from more than one computer (say, at work and at home). The path to which Eclipse deploys your web application is as shown below and has the structure, part of which depends on your application packages obviously, shown:
C:\Users\russ\dev\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps>tree Folder PATH listing Volume serial number is BE79-0AEB C:. +---dvdcatalog � +---images � +---META-INF � +---styles � +---WEB-INF � +---classes � � +---com � � +---etretatlogiciels � � +---dvdcatalog � � +---model � � +---schema � � +---util � � +---validator � +---lib +---ROOT +---WEB-INFFor example, dvdcatalog, my web application, uses a number of user libraries all of which are copied to ..../dvdcatalog/WEB-INF/lib. The name of the user library (as defined by me in Eclipse Build Path) is given in brackets.
04/05/2010 10:44 AM 443,432 antlr-2.7.6.jar 04/16/2010 10:26 AM 322,362 cglib-nodep-2.2.jar [XStream] 04/21/2010 08:29 AM 188,671 commons-beanutils-1.7.0.jar [JSF 1.2 (Apache MyFaces)] 04/21/2010 08:29 AM 46,725 commons-codec-1.3.jar [JSF 1.2 (Apache MyFaces)] 04/05/2010 10:44 AM 559,366 commons-collections-3.1.jar [JSF 1.2 (Apache MyFaces)] 04/21/2010 08:29 AM 571,259 commons-collections-3.2.jar [JSF 1.2 (Apache MyFaces)] 04/21/2010 08:29 AM 143,602 commons-digester-1.8.jar [JSF 1.2 (Apache MyFaces)] 04/21/2010 08:29 AM 76,685 commons-discovery-0.4.jar [JSF 1.2 (Apache MyFaces)] 04/21/2010 08:29 AM 60,686 commons-logging-1.1.1.jar [JSF 1.2 (Apache MyFaces)] 04/05/2010 10:44 AM 313,898 dom4j-1.6.1.jar [Hibernate 3.5.1] 04/05/2010 10:44 AM 3,893,179 hibernate3.jar [Hibernate 3.5.1] 04/05/2010 10:44 AM 597,476 javassist-3.9.0.GA.jar [Hibernate 3.5.1] 04/16/2010 10:26 AM 153,115 jdom-1.1.jar [XStream] 04/16/2010 10:26 AM 56,702 jettison-1.0.1.jar [XStream] 04/16/2010 10:26 AM 534,827 joda-time-1.6.jar [XStream] 04/23/2010 09:29 AM 30,691 jstl-api-1.2.jar [JSTL 1.2] 04/23/2010 09:29 AM 392,435 jstl-impl-1.2.jar [JSTL 1.2] 04/05/2010 10:44 AM 10,899 jta-1.1.jar [Hibernate 3.5.1] 02/23/2010 04:05 PM 391,834 log4j-1.2.15.jar [Log4j] 04/21/2010 08:29 AM 378,921 myfaces-api-1.2.8.jar [JSF 1.2 (Apache MyFaces)] 04/21/2010 08:29 AM 801,254 myfaces-impl-1.2.8.jar [JSF 1.2 (Apache MyFaces)] 03/02/2010 09:22 AM 732,695 mysql-connector-java-5.1.12-bin.jar [MySQL JDBC Driver] 04/05/2010 10:44 AM 23,445 slf4j-api-1.5.8.jar [Hibernate 3.5.1] 04/09/2010 10:30 AM 7,600 slf4j-simple-1.5.11.jar [Slf4j] 04/16/2010 10:26 AM 179,346 stax-1.2.0.jar [XStream] 04/16/2010 10:26 AM 26,514 stax-api-1.0.1.jar [XStream] 04/16/2010 10:26 AM 520,969 wstx-asl-3.2.7.jar [XStream] 04/16/2010 10:26 AM 5,234 xml-writer-0.2.jar [XStream] 04/16/2010 10:26 AM 431,568 xom-1.1.jar [XStream] 04/16/2010 10:26 AM 24,956 xpp3_min-1.1.4c.jar [XStream] 04/16/2010 10:26 AM 431,406 xstream-1.3.1.jar [XStream] 31 File(s) 12,351,752 bytesIf I'm getting ClassNotFound exceptions, something that seems to plague JSTL, and don't have the same libraries on both computers (work and home), this is a place to start asking why.
Appendix: <h:panelGrid>
The preferred way in JSF to create grid alignment is to use this construct. However, it is very weak (JSF 1.2) as compared to, say, <dataTable> in that it's not really possible to get vertical-align: top onto the <td> elements generated. If you don't care to align things to the top, then it doesn't matter. Here's how you should do it:
<h:panelGrid columns="3" rowStyles="query-prompt,query-prompt,query-prompt"> <h:panelGroup style="query-prompt"> <h:panelGrid rowClasses="query-prompt,query-prompt,query-prompt" styleClass="query-prompt"> <h:panelGroup style="query-prompt"> <h:outputText value="#{msg['query.rating_prompt']}" /> <h:selectManyListbox title="#{msg['query.rating_prompt']}" value="#{queryBean.ratings}"> <f:selectItems value="#{queryBean.ratingsItems}" /> </h:selectManyListbox> </h:panelGroup> <h:panelGroup style="query-prompt"> <h:outputText value="#{msg['query.year_prompt']}" /> <h:inputText value="#{queryBean.years}" /> </h:panelGroup> </h:panelGrid> </h:panelGroup> <h:panelGroup style="query-prompt"> <h:outputText value="#{msg['query.genre_prompt']}" /> <h:selectManyListbox title="#{msg['query.genre_prompt']}" value="#{queryBean.genres}"> <f:selectItems value="#{queryBean.genreItems}" /> </h:selectManyListbox> </h:panelGroup> <h:panelGroup style="query-prompt"> <h:outputText value="#{msg['query.language_prompt']}" /> <h:selectManyListbox title="#{msg['query.language_prompt']}" value="#{queryBean.languages}"> <f:selectItems value="#{queryBean.languageItems}" /> </h:selectManyListbox> </h:panelGroup> </h:panelGrid>Instead, despite opinions to the contrary, the good old HTML construct, <table> turned out still to be the best. (Note that CSS class query-prompt contains vertical-align: top.
<table> <tr> <td class="query-prompt"> <%-- column 1 --%> <table> <tr> <td class="query-prompt"> <h:outputText value="#{msg['query.rating_prompt']}" /> </td> <td class="query-prompt"> <h:selectManyListbox title="#{msg['query.rating_prompt']}" value="#{queryBean.ratings}"> <f:selectItems value="#{queryBean.ratingsItems}" /> </h:selectManyListbox> </td> </tr> <tr> <td> <br /> </td> </tr> <tr> <td class="query-prompt"> <h:outputText value="#{msg['query.year_prompt']}" /> </td> <td class="query-prompt"> <h:inputText value="#{queryBean.years}" /> </td> </tr> </table> </td> <td class="query-prompt"> <%-- column 2 --%> <%-- Genre: --%> <table> <tr> <td class="query-prompt"> <h:outputText value="#{msg['query.genre_prompt']}" /> </td> <td class="query-prompt"> <h:selectManyListbox title="#{msg['query.genre_prompt']}" value="#{queryBean.genres}"> <f:selectItems value="#{queryBean.genreItems}" /> </h:selectManyListbox> </td> </tr> </table> </td> <td class="query-prompt"> <%-- column 3 --%> <%-- Language: --%> <table> <tr> <td class="query-prompt"> <h:outputText value="#{msg['query.language_prompt']}" /> </td> <td class="query-prompt"> <h:selectManyListbox title="#{msg['query.language_prompt']}" value="#{queryBean.languages}"> <f:selectItems value="#{queryBean.languageItems}" /> </h:selectManyListbox> </td> </tr> </table> </td> </tr> </table>Of course, this last representation is what gives us:
...instead of seeing the prompts at the bottom of the lists instead of the top.
Appendix: Fixing navigation problems
faces-config.xml describes where link- and button-clicks will take the user, in general, but there is also corresponding information in two places that tie the elements of navigation together.
In this discussion, we'll be talking about exactly three buttons on two pages. The pages inter-refer in that one leads to the other and the other gives the option to return to the first starting over as if just getting there.
Here are two pages, query.jsp and view.jsp. The user tailors the query on the first and clicks either a button, Reset Query, that calls QueryBean.clear() and whose result it isn't necessary to specify here (a sort of internal operation), or Submit Query, which takes him to the viewing page where the results of the query will be displayed.
faces-config.xml
<navigation-rule> <display-name> query </display-name> <from-view-id> /query.jsp </from-view-id> <navigation-case> <from-outcome> submit </from-outcome> <to-view-id> /view.jsp </to-view-id> </navigation-case> </navigation-rule> <navigation-rule> <display-name> view </display-name> <from-view-id> /view.jsp </from-view-id> <navigation-case> <from-outcome> query </from-outcome> <to-view-id> /query.jsp </to-view-id> </navigation-case> </navigation-rule>The JSP file
For the navigation rule above, here's what to expect in terms of JSF code.
query.jsp
One button submits the query as configured on this page navigating to the view page where an <h:dataTable> displays the results. The other button zeroes out the configuration so the user can start over tailoring his query. The second button does not need to "go anywhere" despite a type of "reset".
<h:commandButton value="#{msg['query.submit_button']}" action="#{queryBean.submitQuery}" type="submit" /> <h:commandButton value="#{msg['query.reset_button']}" action="#{queryBean.clear}" type="reset" />view.jsp
This button, Reset Query, returns the user to the previous (query) page.
<h:commandButton value="#{msg['query.reset_button']}" action="#{queryBean.clear}" type="query" />The Java bean
The query bean code establishes the query in a static place where the schema bean, in this case, the one that defines and handles the DVD catalog table, can find and consume it.
Note the strings returned from Java. To a old C programmer, this looks cheesy, but it's critically necessary. These strings must match
- The type attribute in the JSF button (or link) construct of the JSP file as shown above.
- The from-outcome element of the navigation-case in faces-config.xml (also as shown above).
- The return string of the bean action (method) noted in the JSF code. See below.
QueryBean.java
public String clear() { log.info( "Resetting query parameters..." ); this.ratings = null; this.genres = null; this.languages = null; this.sortedBy = null; this.years = null; return "query"; } public String submitQuery() { log.info( "Submitting request..." ); chosenSortedBy = new String( this.sortedBy ); chosenYears = new String( this.years ); int length = 0; String[] newArray = null; length = this.ratings.length; newArray = new String[ length ]; System.arraycopy( this.ratings, 0, newArray, 0, length ); chosenRatings = newArray; length = this.genres.length; newArray = new String[ length ]; System.arraycopy( this.genres, 0, newArray, 0, length ); chosenGenres = newArray; length = this.languages.length; newArray = new String[ length ]; System.arraycopy( this.languages, 0, newArray, 0, length ); chosenLanguages = newArray; log.info( "--------- To be sorted by: " + this.sortedBy ); log.info( "--------- Ratings: " + Arrays.toString( this.ratings ) ); log.info( "--------- Genres: " + Arrays.toString( this.genres ) ); log.info( "--------- Languages: " + Arrays.toString( this.languages ) ); log.info( "--------- Years: " + this.years ); return "submit"; }Troubleshooting
If your navigation doesn't proceed to the expected location, ...
- ensure the three places where the navigation is named are identical,
- ensure that the bean action called will set up, including reset, the underlying application state, and
- ensure that the to-view-id is a real page on the path. (Here, there's no hierarchy: query.jsp and view.jap are in the same subdirectory under Eclipse's WebContent folder. However, it is possible to have deeper and more arbitrary filesystem structure. Make certain this isn't biting you.)
- As a last-ditch effort, rename the navigation (from-outcome) path that doesn't work, say, from "reset" to "query", and try again. Be sure to do this in all three places noted above.
- Eclipse's faces-config.xml editor has a Navigation Rule view that quickly displays the linking between pages including the navigation (from-outcome) path name.
Displaying the results
DvdTitle.java
For reference, here is one working incantation of the getAll() method invoked by view.jsp's <h:dataTable>.
public Result getAll() throws SQLException, NamingException { log.info( "Performing SQL select to build view.jsp..." ); try { ResultSet rs = null; Result r = null; String query = null; String[] ratings = QueryBean.getChosenRatings(), genres = QueryBean.getChosenGenres(), languages = QueryBean.getChosenLanguages(); String years = QueryBean.getChosenYears(), sortedBy = QueryBean.getChosenSortedBy(); openConnection(); Statement stmt = this.conn.createStatement(); if( ( ratings == null || ratings.length == 0 ) && ( genres == null || genres.length == 0 ) && ( languages == null || languages.length == 0 ) && ( years == null || years.length() == 0 ) ) { // the query isn't to be qualified in any way... query = "SELECT * FROM " + TABLE_NAME; if( sortedBy != null ) query += " ORDER BY " + sortedBy; query += ";"; log.info( query ); rs = stmt.executeQuery( query ); r = ResultSupport.toResult( rs ); if( r == null ) log.info( "Query returned nothing." ); return r; } // these are crucial to helping us build SQL syntax in the query... boolean gotRating = false; boolean gotGenre = false; boolean gotLanguage = false; // the query will be qualified in some way... query = "SELECT * FROM " + TABLE_NAME + " WHERE "; if( ratings != null && ratings.length > 0 ) { // Ratings, could be one or more: [g, pg, pg-13, r] for( String rating : ratings ) { query += ( gotRating ) ? " OR " : "( "; query += RATING + " = '" + rating + "'"; gotRating = true; } if( gotRating ) query += " )"; } if( genres != null && genres.length > 0 ) { if( gotRating ) query += " AND "; // Genres, could be one or more: [action, drama, history, music, politics, suspense] for( String genre : genres ) { query += ( gotGenre ) ? " OR " : "( "; query += GENRE + " = '" + genre + "'"; gotGenre = true; } if( gotGenre ) query += " )"; } if( languages != null && languages.length > 0 ) { if( gotRating || gotGenre ) query += " AND "; // Languages, could be one or more: [su, el, it, ja, pt] for( String language : languages ) { query += ( gotLanguage ) ? " OR " : "( "; query += LANGUAGE + " = '" + language + "'"; gotLanguage = true; } if( gotLanguage ) query += " )"; } /* Do years stuff here--very ugly, very nasty. * * What we support, e.g.: * single year, i.e.: 2010 * year range, i.e.: 1969-1993 * year list, i.e.: 1963, 1983, 1997 * * What is used here, no matter how simple or complex, is an array. */ String[] yearList = getYearQuery( years ); if( yearList != null && yearList.length > 0 ) { if( gotRating || gotGenre || gotLanguage ) query += " AND "; if( yearList.length == 3 && yearList[ 1 ].equals( "-" ) ) { // manufacture a BETWEEN query query += YEAR + " BETWEEN '" + yearList[ 0 ] + "' AND '" + yearList[ 2 ] + "'"; } else { // manufacture an OR ... OR ... OR ... query boolean gotYear = false; for( String y : yearList ) { query += ( gotYear ) ? " OR " : "( "; query += YEAR + " = '" + y + "'"; gotYear = true; } if( gotYear ) query += " )"; } } if( sortedBy != null ) query += " ORDER BY " + sortedBy; query += ";"; log.info( query ); rs = stmt.executeQuery( query ); r = ResultSupport.toResult( rs ); if( r == null ) log.info( "Query returned nothing." ); return r; } finally { close(); } }view.jsp
Here's how to display the results in JSF.
<h:dataTable value="#{dvdTitle.all}" var="_title" styleClass="dvdStyles" headerClass="dvdHeader" rowClasses="oddRow,evenRow"> <h:column> <div style="margin-left: 10px"> <p> <span style="font-style: italic"> <h:outputText value="#{_title.title}" /> </span> <h:outputText value="(#{_title.year})" /> </p> <p> <h:outputText value="#{_title.minutes} #{msg['view.minutes.prompt']}" /> </p> <p> <h:outputText value="#{_title.rating}" /> <%-- >h:outputText value=", #{_title.genre}" /< --%> </p> <p> <h:outputText value="#{msg['view.languages']} " /> <h:outputText value="#{_title.language}" /> </p> <p> <h:outputText value="#{msg['view.site.prompt']}" /> <h:outputText value="#{_title.url}" /> </p> <p><h:outputText value="#{msg['view.stars']}" /></p> <p style="margin-left: 20px"> <h:outputText value="#{_title.stars}" /> </p> </div> </h:column> </h:dataTable>Getters take no argument
Early on, in imitation of view.jsp's value attribute to <h:dataTable>, "#{dvdTitle.all}", I unthinkingly added such a reference to an additional method that looked for just one title and returned a Result. The problem was that this method originally took String title. Hence, it was not a "property". I got:
ERROR - An exception occurred org.apache.jasper.el.JspPropertyNotFoundException: /view-one.jsp(38,1) '#{dvdTitle.single}' Property 'single' not found on \ type com.etretatlogiciels.dvdcatalog.schema.DvdTitleLater, as I struggled with it, I began to get another warning in the JSP:
'single' cannot be resolved as a member of 'DvdTitle'This was solved of course by removing the argument. A property, by definition, has a getter and a setter. This is a getter and must have no argument. Ultimately, peace ensued in view-one.jsp:
view-one.jsp
. . . <%-- usually this is just one other film, but it can be more --%> <h:dataTable value="#{dvdTitle.single}" var="_title" styleClass="dvdStyles" headerClass="dvdHeader" rowClasses="oddRow,evenRow"> <h:column> . . .At this point, view-one.jsp presents the option of adding the new title despite its already existing or returning to addtitle.jsp to add a different title.
We're obviously adding the power user stuff (by which the power user can add titles to the database). Later, addtitle.jsp will be linked to via a button from powerlogin.jsp.
Solution to <h:commandButton...> in view-one.jsp
The button "not to add" a duplicate DVD title to the database on this page would not work with type as "reset" and, of course, the button to add it anyway was typed "submit". The solution, after exploring immediate="true", changing the bean type for AddBean to "request" or "none" from "session", etc. was the third possible option, that is, "button". Prior to this, clicking on it resulted in nothing, not even AddBean.clear() was called.
See discussion at "No transition next page button".
<%-- Don't Add This Title --%> <h:commandButton value="#{msg['view-one.reset_button']}" action="#{addBean.clear}" type="button" /> <%-- Add This Title --%> <h:commandButton value="#{msg['view-one.confirm_button']}" action="#{addBean.retainAddition}" type="submit" />The final navigation
Here's a representation of the final details of application navigation (between pages).
What's left to do?
The following, whether we do them or not, are left to do to make this a real application.
- Add the ability to identify, then delete unwanted titles by the power user.
- Flesh out the genre and language tables and connect them up with the titles table. Write SQL statements in consequence. Answer the question, "Do these need a junction table?"
- Move authentication to the (or a) database.
- Enhance messaging in the application (via <h:message[s]...>, etc.)
- Deploy to Tomcat 6.