RSF Developer's Notebook
EDIT (2008-08-06): A good portion of this guide no longer is accurate. Please take that into consideration if you use it.
RSF - a Sakai Developer's Notebook
I. Introduction
This document is an attempt at coalescing information gathered about the Reasonable Server Faces (RSF) Java framework from its Wiki, forums and emails from the framework developers. In such a document there will be errors in what is stated. My hope is to create a starting point for information about RSF and thus allow this to be a living document growing as the information changes and is corrected.
This document should in no way be looked upon as an authoritative guide to RSF. For that authoritative knowledge, please do visit the RSF site itself http://www2.caret.cam.ac.uk/rsfwiki/. This guide is merely the typed of notes of developers' experiences with RSF hopefully in conversational communication style that will be easy for other developers to understand using simple examples to demonstrate concepts.
This document covers specifically developing in RSF within Sakai. So although the information here could be generalized for RSF outside of Sakai, its goal is to document things within Sakai. It is intended to be an example based document that is short on theory and long on hands on examples. If you hunger for RSF theory, again, visit the RSF site http://www2.caret.cam.ac.uk/rsfwiki/. I guarantee that visiting there will give you as much theory as you can handle. Good stuff!
BTW - Mark Norton gives an excellent overview of RSF at http://confluence.sakaiproject.org/confluence/display/MJN/RSF+in+Sakai
II. Eclipse Plugin
RSF Sakai applications tend to have a fairly complicated directory structure. Although this doesn't need to be this way it appears that this is a best practices type thing. Therefore it seems wise to follow. Luckily, the RSF framework developers have created an Eclipse IDE plugin that handles creating the directory skeleton for us. Do take note that this document assumes that you are developing with Eclipse 3.2+, have Maven 2 and Subversion installed. Throughout this document we refer to "Maven". These references actually mean to imply "Maven 2" and not "Maven 1". We also assume that you are a somewhat experienced Java developer. You should be familiar enough with Sakai to have an installed Tomcat instance running. We will briefly cover in this document retrieving, compiling and deploying the Sakai Cafe installation.
The Eclipse Plugin can be easily installed using Eclipse's built in Feature Install facility. Follow screenshots 1-10 to observe the procedure.
Screenshot 1 - Find and Install Software updates
Screenshot 2 - Search for new features to install
Screenshot 3 - New Remote Site
Screenshot 4 - New Update Site. URL to be entered is: http://source.sakaiproject.org/appbuilder/update/
Screenshot 5 - Select RSF App Builder Update site and click "Finish"
Screenshot 6 - Select the Sakai App Builder feature and click "Next"
Screenshot 7 - Accept the terms of the license agreement.
Screenshot 8 - Finish the installation configuration
Screenshot 9 - After the plugin downloads, click "Install All"
Screenshot 10 - Restart Eclipse for the new software to be ready for use
III. Obtaining the Sakai Cafe
We are going to use the Sakai Cafe as our base Sakai install with which to develop. The Sakai Cafe seems to be a minimum set of Sakai tools needed to get a base Sakai installation up and running for development. You should be able to use any Sakai version but we'll use Cafe here so that we don't obtain more than we need.
In Screenshot 11, I created a subdirectory in my home directory called svn. This is where I usually put stuff I obtain from svn. This directory name is completely arbitrary.
I then use subversion to obtain the cafe trunk and place this in a subdirectory of my svn subdirectory called "cafe".
Screenshot 11 - Obtain the Cafe
When the subversion download is complete we should cd into the cafe directory. We can then try to build the cafe. But notice the error we get in Screenshot 12? For some reason we need to compile the "master" project first before we can compile the rest of Sakai Cafe. This is quite easy.
Screenshot 12 - Trying to compile the Sakai Cafe
Just cd into $HOME/svn/cafe/master and issue "mvn install". That should build the master project. Then cd back into the main cafe directory ($HOME/svn/cafe) and issue the maven build line similar to the one included in Screenshot 12.
Now we need to import this cafe distribution into Eclipse. To do this we go to the File menu of Eclipse and select "Import"
Screenshot 13 - Importing the Cafe into Eclipse
Screenshot 14 - Import all projects under the Cafe directory
After you import all these projects into Eclipse you're going to notice that a bunch of red "X's" appear on each project. This is because dependency libraries cannot be found by Eclipse. Sakai uses Maven for its build. And Maven places dependences into the default folder $HOME/.m2/repository. We need to let Eclipse know where these dependencies are located as Eclipse has no way of knowing that we use Maven to build. To educate Eclipse, we go to the Eclipse's "Window" menu and select "Preferences". Then expand the "Java" tree item on the left side of the newly opened "Preferences" window. Under the expanded "Java" tree menu is another expandable menu titled "Build Path". Expand that. Then click on ClassPath Variables". We need to add a couple of variables to a) let Eclipse know where the Maven repository is and b) a dummy variable to apparently allow the Sakai RSF App Builder to function. Click on "New" to add a new variable. Add a variable named "M2_REPO". Assign its value to that $HOME/.m2/repository directory that we spoke of earlier. In my case it is /home/dsobiera/.m2/repository. After adding this, Eclipse should be fine with those dependencies libraries (the little "X's" in the imported Sakai Cafe projects). But we need to add another variable called "MAVEN_REPO". This one is so that the RSF Sakai App Builder plugin works without complaining. For some reason the plugin wants to use "Maven 1" for some of its functionality. So I set it to some arbitrary destination as I never use Maven 1. If you use Maven 1 you may want to research this further. Create a new classpath variable following the same steps for the just created M2_REPO. Assign its value to be the same as M2_REPO. For what it is worth, I don't think MAVEN_REPO needs to point to anywhere useful. It just needs to be defined. Once done with adding these variables, click "OK" to close the Preferences window. Eclipse will then rebuild its workspace. Once completed, those little red "X's" should be gone.
Screenshot 15 - Preferences Window and adding Maven Classpath variables
IV. Creating a New Sakai Application project
Screenshot 16 - Creating a New Sakai Application Project
Screenshot 17 - Select the Sakai App Builder to build the project
Screenshot 18 shows the values to fill in here. Make sure to uncheck the "use default location" and select for the location the location of your Sakai Cafe svn download. Otherwise the Sakai App project builder will build the new project in the Eclipse workspace and not as a subdirectory in the Sakai Cafe directory. This is needed for Sakai builds to happen correctly. We select the Full CRUD (Create, Retreive, Update and Delete) app as the type to create because a) we want to make sure the Sakai app Builder can create a working RSF app and b) whenever we build a new app we would like the directory structure AND the configuration files stubbed. If we select "Directory Structure Only" we do get the directory structure skeleton created. But important configuration files like application and request context files are not built.
Screenshot 18 - Creating a New Sakai Application Project
CD into the newly created $HOME/svn/cafe/Helloworld01 directory and use maven to build and deploy this new application.
Screenshot 19 - Building our newly created CRUD application and deploying it to Tomcat
Whoops. We notice a build error. This is because the Sakai app builder set the Maven reference to the master project version to "M2" for this Helloworld01 project.
Screenshot 20 - Wrong version of master project build error
Sakai Cafe's master wants to be version "SNAPSHOT" not "M2". So look for the line in screenshot 21 and fix the master project's version reference from M2 to SNAPSHOT. This would be done in the file $HOME/svn/cafe/Helloworld01/pom.xml
Screenshot 21 - Fixing the reference to the master project version
Save this change. Try to build and deploy our new project with Maven. All should work well now.
Create a New Sakai project worksite.
This will help group the RSF tools we develop into an easy to find area.
Screenshot 22 - Creating a new worksite for RSF
Screenshot 23 - Add the Helloworld01 tool to the RSF project worksite
Screenshot 24 - Add the Helloworld01 tool to the RSF project worksite
Now that we know Sakai and the RSF builder worked, we can prepare the project for our own use. It will require removing a few items in the Helloworld01 project. We need to delete the following files:
$HOME/svn/cafe/Helloworld01/tool/src/java/org/sakaiproject/helloworld01/tool/params/AddItemViewParameters.java
$HOME/svn/cafe/Helloworld01/tool/src/java/org/sakaiproject/helloworld01/tool/producers/AddItemProducer.java
$HOME/svn/cafe/Helloworld01/tool/src/java/org/sakaiproject/helloworld01/tool/producers/ItemsProducer.java
$HOME/svn/cafe/Helloworld01/tool/src/java/org/sakaiproject/helloworld01/tool/ItemsBean.java
$HOME/svn/cafe/Helloworld01/tool/src/webapp/css/Helloworld01.css
$HOME/svn/cafe/Helloworld01/tool/src/webapp/templates/AddItem.html
$HOME/svn/cafe/Helloworld01/tool/src/webapp/templates/Items.html
$HOME/svn/cafe/Helloworld01/tool/src/webapp/templates/Messages.html
$HOME/svn/cafe/Helloworld01/tool/src/webapp/WEB-INF/applicationContext.xml
$HOME/svn/cafe/Helloworld01/tool/src/webapp/WEB-INF/requestContext.xml
Screenshot 25 - Delete the example files created by the Sakai App Builder to clear this out for our own development (ItemsBean.java, AddItemViewParameters.java, AddItemProducer.java and ItemsProducer.java)
Screenshot 26 - Remove more example files created by the Sakai App Builder to clear this out for our own development (Messages.html, Helloworld01.css, AddItem.html and Items.html)
Then download the below files and place the files at:
$HOME/svn/cafe/Helloworld01/tool/src/webapp/WEB-INF
These files are used to configure a RSF application. So we will be adding to these files as we build our new application.
Screenshot 27 Replace the applicationContext.xml and requestContext.xml files with these: applicationContext.xml requestContext.xml
Now we want to create a webpage for our new web application. Webpages in RSF must be XHTML. Which means, every opening tag must have a closing tag. Go into Eclipse and create a main.html file as shown in the screenshots below.
Screenshot 28 - Create a file
Screenshot 29 - Create main.html
<html> <head> </head> <body> <div>Hello World Text </div> </body> </html>
This main.html is called in RSF lingo a "template". In RSF, we need something called a "view producer" attached to each template. By convention it is good to name the view producers the same as the template that it is associated with. But this is not requited. Create a Java class and replace the Eclipse generated skeleton for the new class with below.
Screenshot 30 - Create a class
Screenshot 31 - Create a MainProducer class
package org.sakaiproject.helloworld01.tool.producers; import uk.org.ponder.rsf.components.UIContainer; import uk.org.ponder.rsf.view.ComponentChecker; import uk.org.ponder.rsf.view.DefaultView; import uk.org.ponder.rsf.view.ViewComponentProducer; import uk.org.ponder.rsf.viewstate.ViewParameters; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class MainProducer implements ViewComponentProducer, DefaultView { private static Log logger = LogFactory.getLog(MainProducer.class); // The VIEW_ID must match the html template (without the .html) public static final String VIEW_ID = "main"; public String getViewID() { return VIEW_ID; } // end getViewID() public void fillComponents(UIContainer tofill, ViewParameters viewparams, ComponentChecker checker) { } // end fillComponents() } // end class MainProducer
Most of this will become clear as we develop our application. Just accept it on faith now that all this is needed. The one piece to learn about now is the line that reads:
// The VIEW_ID must match the html template (without the .html) public static final String VIEW_ID = "main";
As the comment states, the VIEW_ID must be set to the template's base name. Hence why we created main.html in our last step.
Remember those two configuration files that we deleted and then replaced a few steps ago? Now we get to edit one of them. Every view producer MUST be defined in requestContext.xml. A common mistake is to create the template and producer, compile and run without adding this producer to requestContext.xml.
Edit $HOME/svn/cafe/Helloworld01/tool/src/webapp/WEB-INF/requestContext.xml with the following
Screenshot 32 - Compile Helloworld01
Now let's compile and deploy the application.
Screenshot 33 - Compile Helloworld01
Okay, this application doesn't do much. But it does display the template that we created.
Now let's start doing something somewhat interesting. So far our main.html template looks like it is just plain XHTML. It is. But there is something extra we can add to the template that lets RSF do something other than just render the template "as written". We are going to add our first RSF piece into the main.html template. RSF uses the attribute "rsf:id" in the XHTML tags. Add the highlighted text on the screenshot below to the div tag in main.html.
Compile and run the application.
Whoa...what happened? Now we aren't even getting the text in the div. This rsf:id attribute did something. It did indeed. RSF leaves XHTML tags in a template unchanged unless told to do otherwise. So how do we tell RSF to do something other than render the template "as is"? When we place a rsf:id attribute in a XHTML tag, RSF treats this in a most different way. The reason we lost our text is that the rsf:id="helloworldtext" attribute gave a RSF ID of "helloworldtext". When RSF couldn't find this ID it doesn't render the tag (and anything in between the opening and closing tag) that has this undefined rsf:id. So the div tag wasn't rendered at all. Go check the source of that "empty text" page. There will be no div tag in there.
So we need to somehow let RSF know about this helloworldtext ID. How do we do this? We do this in the view producer. Go into MainProducer.java and add the following. One is an import statement and the other is in a method called fillComponents.
As you can probably tell, we have added something called a "component". And we added this in a method called fillComponents. Although probably not entirely accurate I like to think of fillComponents rather like paint() is for Java AWT applications. Remember that rsf:id="helloworldtext" XHTML attribute in that div tag in main.html? In RSF vocabulary it is said that this div tag peers with this UIOutput component. You will hear often of peer tags in RSF circles so it is best to start using this term now. And a UIOutput component is one that....well...outputs things. We use the make method with a few parameters to create this component. The parameters are as follows:
tofill - this is the parent container for this component to reside in. In our specific case, it is a parameter coming into fillComponents. For now, think of this particular parameter as the top level page container.
"helloworldtext" - this is the RSF ID in our peer tag
"This is the NEW text" - the text to substitute in the peer tag
Notice that the text that we want substituted is "This is the NEW text" and what was in the main.html template was "Hello World Text". RSF will replace the tags that have RSF ids given to them with (and text between the opening and closing tags) with their peered components. Compile, deploy and run our Sakai tool.
Up to this point we've been concerned about text output. But as we all know, forms are the bread and butter of web applications. So it seems appropriate to do that now.
Let's create a simple form. It has a text input field and a submit button. Let's create this form in main.html
So we added a form tag with RSF ID "form1", a text input tag with a RSF ID "input1" and a submit button with a RSF ID "submit1". And, as you should now know, these new tags (and new RSF ID's) need components in the producer to peer with. Let's add the import statements for these new components.
Then let's add the components for these new RSF ID's. We need a form components, an input component and a command component.
These look similar to what we did for UIOutput. Let's go through these line by line.
UIForm form1 = UIForm.make(tofill, "form1");
We are using the make method to create the component. We give make as parameters the parent container of the form and like the previous UIOutput it is the top level container tofill. We then assign the RSF ID of "form1" to this component so that it can peer with the form XHTML tag in our main.html template. Do take note that unlike UIOutput, we assign the UIForm to a Java variable after issuing the make. That is so that we can have a reusable reference to this. We will use this form1 variable reference to use it as the parent container for our other form elements.
UIInput.make(form1, "input1", "#{backbean1.input1}");
This creates a UIInput component. Notice that the parent parameter is form1 and not tofill. This is because this component's parent container is the form and not the top level container. Makes sense, right? The second parameter to make is the RSF ID "input1". The third parameter is a slightly odd one. What is this? Well in a form submission in RSF the form elements need to be stored somewhere. And what we do here is use a syntax that tells the component where to store the input1 text that is entered. The syntax is something called an EL (Expression Language). What this says is put this input1 data into a Java bean called backbean1 and specifically, into the input1 member variable of this backbean1. We haven't defined the backbean1 yet. After we finish explaining the producer that is our next step. Note that the EL is surrounded by #{}. This is not NECESSARY. We could have just put "backbean1.input1". However, by using the #{} this helps us visually identify whether something is an EL or just text. So let's be consistent and use "#{backbean1.input1}". It is easy to see now on visual inpection of this code that this is an EL.
UICommand.make(form1, "submit1", "#{backbean1.submit1}");
This creates a UICommand component. This usually peers with a XHTML submit button. Again notice that the parent container is form1 and not the top level tofill. The 2nd parameter assigns this component an RSF ID of submit1. The third parameter is a now familiar EL expression. But submit1 is not a member variable in this case. UICommands run....uhhh...commands. So submit1 is a method in the backbean1. This will become clearer when we create the backbean1. So let's do that now.
We are just going to create a POJO (Plain Old Java Object). It doesn't inherit from any RSF piece. It doesn't know anything about RSF. It just is. No magic here. Outside of the logging functionality there isn't anything but stock Java here. This type of JavaBean in RSF is called a backing bean. Again, it is a term often used in RSF circles so it is best to start using this term.
We created a member variable called input1 with an appropriate getter and setter. We also created a method called submit1. In method submit1 all we do is output that we are in submit1 and what the submitted value of input1 was. Not much data processing here. But it is easy to imagine something far more complex in this method.
We need to remember those two configuration files (applicationContext.xml and requestContest.xml) constantly when we devleop in RSF. Although this BackBean1 knows nothings of RSF we need to make RSF aware of BackBean1. So a good rule of thumb to know when we should place something in the configuration files is if RSF needs to know about them, we need to add them. So far we have view producers and now backing beans. We first need to add it to the requestcontext.xml file as shown below
Notice the id="backbean1" before the class=? This is different than the producers. This "id", think of it as an alias. This is what RSF uses to reference this backing bean via EL. I tend to use lower case in the id just so that I can distinguish it from the class name itself. But it is a personal preference and completely arbitrary. But being consistent is obviously important. So remember exactly what you put in here.
We need to do something else special with backing beans. Since backing beans get user data we need to make sure that they are allowed to do so. By default, no backing bean is allowed to accept data in RSF. To allow this backing bean to accept data in our application we need to add it to the requestAddressibleParent area of applicationContext.xml. You'll see a lot of blah blah blah in this file. Look for the area that has parent="requestAddressibleParent". In the property named "value" assign the value of "backbean1" as shown below. Make sure to use the exact id/alias that you assigned it in requestContext.xml. Also note, if you have more than one backing bean you would separate them by commas in this field. So if we had 3 different types of backing beans this area would look like:
<bean parent="requestAddressibleParent"> <property name="value" value="backbean1,backbean2,backbean3" /> </bean>
Since we only have one type of backing bean we just put backbean1 in there.
We are now ready to see the fruits of our labor. Compile and deploy the application to Sakai. We'll notice the form presented as our Helloworld01 tool starts.
Let's enter some text into the input field and press the submit button.
You'll notice that after we press the submit button that we are presented with the same page that had our form on it with the input field emptied. Huh? This is normal. It is because we didnt tell RSF to change to a different view after the submit. We'll cover how to do that in a little while.
So one might say - "So how do I know this did anything?" Well, if you look in your $TOMCAT_HOME/log/catalina.out file you should see something that looks like this:
YAY!! Our logging message in the backing bean fired off.
So now we'd like our form when submitted to go to a different page. How do we do that? It's actually pretty easy. We need to create a new page to go to. And as we know, a "page" in RSF is a template and a view producer. Let's create a results.html:
<html> <head> </head> <body> Your form has been submitted </body> </html>
And, of course, a ResultsProducer:
package org.sakaiproject.helloworld01.tool.producers; import uk.org.ponder.rsf.components.UIContainer; import uk.org.ponder.rsf.view.ComponentChecker; import uk.org.ponder.rsf.view.ViewComponentProducer; import uk.org.ponder.rsf.viewstate.ViewParameters; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class ResultsProducer implements ViewComponentProducer { private static Log logger = LogFactory.getLog(ResultsProducer.class); // The VIEW_ID must match the html template (without the .html) public static final String VIEW_ID = "results"; public String getViewID() { return VIEW_ID; } // end getViewID() public void fillComponents(UIContainer tofill, ViewParameters viewparams, ComponentChecker checker) { } // end fillComponents() } // end class ResultsProducer
We need to point out two things. One - obviously we need the VIEW_ID to be "results" in the ResultsProducer so that the results.html template will be associated with this producer. And two, we need to revisit the class definition for a moment.
Here is MainProducer:
public class MainProducer implements ViewComponentProducer, DefaultView
Implements ViewComponentProducer will be for every view producer. But DefaultView should only be for....well...your default "page". There should only be one default page so only MainProducer should implement this. This is how RSF knows what view to start the web application. It starts with the view producer designated as the default view.
This means that our ResultsProducer should NOT implement DefaultView.
public class ResultsProducer implements ViewComponentProducer
Make sure to remember that since we added a new view producer to our web app we have to list it in requestContext.xml
So are we ready to submit the form? No, not yet. We have to let the MainProducer know that after the form is submitted it needs to go to the results' view. We need to edit the MainProducer. We need to add some import statements, make our class implement something else and then define the different view.
We need to add a navigation case for our ResultsProducer. We should add one for MainProducer as well in case we ever want to make sure we can go explicitly back to the MainProducer after form submit.
So NOW are we ready? Almost. But somehow we have to return back a different string key so that the proper navigation case happens. This happens in our BackBean1's submit1 method.
Remember submit1 returned void? Well now we need to change the signature of this method to return a String.
public String submit1() { logger.info("BackBean1: I'm in submit1. Input1 = " + input1); return "results key"; }
If the string key returned by submit1 isn't a navigation case that we defined in MainProducer the current view will be used. But "results key" was defined by us and therefore should go to the results page.
Anyways, compile and deploy. When you submit your form you should now get your results page.
Okay, so we have a results page. But we'd like to see the input value we submitted.
Well, here's where it gets a bit tricky. From what I can tell, RSF doesn't work like web languages I've used before. It doesn't keep things in what we normally think of as request scope for very long. Between the BackBean1 and the results navigation case the bean itself isn't persisted. Go to the RSF wiki and read about action cycle and render cycle to get a better idea of why this is. From my point of view, it appears RSF wants to use a database on the back end for persisting and retrieving. So in submit1(), RSF seems to prefer that we persist the form values to a database and then in ResultsProducer's fillComponents() it would be read out of the database. So there's no way to do this without persisting the data? Well, actually there is. I'll show it to you but it seems kind of unnatural for RSF to do it this way.
We need to use Spring's dependency injection to inject the BackBean1 into the ResultsProducer. RSF uses Spring throughout. If you're not used to Spring and dependency injection this will seem a tad odd. What this is doing is allowing us to use BackBean1 like it is a member variable of the ResultsProducer so that our fillComponents() can obtain the value of input1. It actually is easier to do than to explain. In requestContext.xml make this change:
The name is the Java member variable name this will map to (case sensitive). The ref is the id/alias of the BackBean1 backing bean declared earlier in this file.
And in ResultsProducer add this:
So now we should be able to access in fillComponents() input1 as easy as backBean1.getInput1().
But here's where the RSF rub comes in. Since RSF will lose anything in what is often known as the request scope when we get to ResultsProducer, BackBean1 is going to be "empty" (actually the values will be init'd to default). So how do we get this to persist?
We can temporarily put the BackBean1 into session scope. As I said earlier, this seems clumsy and I assume this is for a reason. What we are doing here doesn't seem to be how RSF wants to work. It seems like it wants to persist and retrieve from a database of some sort. That would follow with one of RSF's goals being trying not to depend on state. But I figured I'd show this just for kicks and giggles.
So to put the backbean1 into the session we need to add this to applicationContext.xml:
Add the following to your results.html:
And this to your ResultsProducer
Compile, deploy and run. Submit the form and voila. Of course, the BackBean1 is now is session scope and persists. So we'd need to clean up the session after the input1 value is taken out to UIOutput. So although this works, it just seems like it's not really how RSF was intended to work. These session scoped beans seem like they're more for shopping cart type things then for displaying the results of a form submission.
Let's remove what we just did so that we can put our little application into a state that we can build upon it some more. Let's reverse the steps we just did to get session scope form submission to work. So in the next 6 screen shots REMOVE the highlighted text from the specified file.
Now our application is in a good state again to do further work. So from this point on highlighted text in screenshots is to be added not removed (unless told otherwise).
<Place holder for branch containers>
<Place holder for view parameters>
<Place holder for decorators>
<Place holder for messages and bundles>
<Place holder for BeanLocators and OTP>
<Place holder for AJAX>
UPDATE (2008-05-08) - I just finished my first rsf project with my team. YAY!!!! We all have learned a lot about RSF since the writing of this guide. And I expect that learning will continue. I plan on this summer updating this guide with a friendlier and more complete version. This was a good starting guide but a) it's loooong and b) some information is not entirely how I would explain it now. So, this guide will be replaced with a new one over the Summer of 2008. I am waiting for RSF 0.7.3 to be released as a lot of my project I was working on involved AHAH and it would be useful to have a version of RSF officially out that supports what we did.