Table of Contents
The Spring application is being developed in a clean version of Eclipse with the Spring IDE plug-ins installed. I’m using Spring 2.5.4 and Web Flow 2.0, and most of the required libraries are coming from the spring dependencies.
To Start with, I’ll create a new plain old vanilla
dynamic web application. I could add in the JPA or Faces
facets, but then I have to start configuring the
libraries in Eclipse and it’s just easier to do it
manually since I’ll be adding all my other libraries
manually. I added the elements to
web.xml
to configure faces, facelets, spring, and spring web
flow. As I deploy it I need to add libraries for the
features, so I gradually add the 30 or so needed
libraries. I then add the
persistence.xml
file to the classpath, and my
log4j.xml
config file. I configure the
applicationContext.xml
to include three other config files, one for JPA and one
for Spring Web Flow, and the other for my actual beans.
I’m using MySQL for the database and using JPA in both
examples. It can take anywhere from 15 minutes to an
hour to get the application deployed and running
depending on experience and how much trial and error you
employ in finding out which jars you need. Having been
through this a number of times before, it took me closer
to 15 since I just copied over most of the required
files.
Once I have the application created, and I can deploy
and run it, I create the model, namely the
Project
,
Issue
and
IssueStatus
classes with the JPA annotations. For simplicity, I am
using the
create-drop
JPA functions to create the database schema and also
using
import.sql
to insert some test data.
We also have a
projectDao
interface that contains the methods for dealing with
Project
objects.
Example 2.1.
ProjectDao
interface
public interface ProjectDao {
Project findProject(Long projectId);
Project refresh(Project project);
List<Project> findProjects();
void save(Project project);
}
This is implemented in a class called
ProjectDaoBean
Example 2.2.
ProjectDaoBean
implementation
public class ProjectDaoBean implements ProjectDao,Serializable {
@PersistenceContext
private transient EntityManager entityManager;
@Transactional
public Project findProject(Long projectId) {
return entityManager.find(Project.class, projectId);
}
@Transactional
public List<Project> findProjects() {
return entityManager.createQuery("select p from Project p").getResultList();
}
@Transactional
public Project refresh(Project project) {
entityManager.refresh(project);
return project;
}
@Transactional
public void save(Project project) {
entityManager.persist(project);
}
}
This is really simple code, and shouldn’t need too much
explanation. The entity manager is defined as transient
since spring web flow will complain about the entity
manager not being serializable as it tries to serialize
the bean. We add the
projectDao
bean to the Spring Bean configuration.
Example 2.3. Spring Bean Definition
<bean name="projectDao" class="org.issuetracker.dao.ProjectDaoBean"/>
Once our application is up and running, it is time to
start creating some pages. For reasons that will become
clear later on, we will put our project list in its own
page flow. We create a new folder
WEB-INF\WebContent\flows\projects\
and in it we add a new file called
projects.xml
which will contain our new web flow.
Example 2.4.
projects.xml
flow definition.
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd"
start-state="projects">
<view-state id="projects">
<on-render>
<evaluate expression="projectDao.findProjects()"
result="flowScope.projects" result-type="dataModel" />
</on-render>
<transition on="edit" to="projectEdit" />
<transition on="view" to="projectView" />
</view-state>
<subflow-state id="projectEdit" subflow="projectEdit">
<input name="projectId" value="projects.selectedRow.id" />
<transition on="cancel" to="projects"/>
<transition on="save" to="projects"/>
</subflow-state>
<subflow-state id="projectView" subflow="projectView">
<input name="projectId" value="projects.selectedRow.id" />
<transition on="close" to="projects"/>
</subflow-state>
</flow>
SWF lets you customize the names for flows, pages and
the url used to invoke it, but it uses sensible defaults
such as the state name. For now, I've used this default.
The start view is the view state called
projects
, which calls
findProjects()
on my stateless bean when the page is rendered, and
wraps the result in a JSF data model and puts it in a
flowScope
variable. The flow scope is a scope which lasts for the
life of the flow, and
projects
is the name of the variable. Note that we can now refer
to this variable as
#{projects}
since all scopes will be searched until the variable is
found. We have two transitions from the projects page,
edit
and
view
which takes us to the edit and view states which are
defined as subflows. We could define the edit/view
process as part of this projects flow, but the problem
with that is we cannot re-use those flows from other
places if we do that. Since we want to provide direct
access to the view and edit pages via a URL, we have
chosen to put them in separate flows.
In each subflow, we define an input variable called
projectId
which is the Id of the selected project. Since we use a
dataModel as the source of data for the table, we get
clickable tables where the selected row in the data
model is set based on the item clicked in the table that
caused the postback.
Example 2.5.
projects.xhtml
page
<h:messages globalOnly="true" styleClass="message" />
<h:form>
<h:dataTable value="#{projects}" var="v_project">
<h:column>
<f:facet name="header">ID</f:facet>
<h:outputText value="#{v_project.id}" />
</h:column>
<h:column>
<f:facet name="header">Title</f:facet>
<h:commandLink value="#{v_project.title}" action="view"/>
(<h:commandLink value="Edit" action="edit"/>)
</h:column>
</h:dataTable>
</h:form>
We reference the variable
#{projects}
which refers to the projects variable we set up in the
on-render
stage in the page flow. We display the title of the
project as a link which returns the action
view
and we have an edit link which returns the
edit
action. These actions mean nothing in the page itself,
they only have meaning in the faces navigation, or in
our case, the Spring flow. Looking at the flow, an edit
or view action transitions to the subflows to edit or
view the project and they have the project Id passed in
to them. With that in mind, lets look at the project
edit subflow.
Example 2.6.
projectEdit.xml
subflow.
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd"
start-state="projectEdit">
<persistence-context />
<input name="projectId" value="requestScope.projectId" />
<on-start>
<evaluate expression="projectDao.findProject(projectId)"
result="flowScope.project">
</evaluate>
</on-start>
<view-state id="projectEdit">
<transition on="save" to="save">
<evaluate expression="projectDao.save(project)" />
</transition>
<transition on="cancel" to="cancel" />
</view-state>
<end-state id="save" view="externalRedirect:/projects" commit="true"/>
<end-state id="cancel" view="externalRedirect:/projects" />
</flow>
The
persistence-context
element at the start lets us use the same entity manager
for this flow. This means we can load, modify and save
the entity in this flow and it will use the same entity
manager so we don't have to worry about the entity
becoming detached. The
input
element declares
projectId
as an input value. The
projectId
variable can be passed in from a parent flow, but if it
is not, then the value is assigned from the request
parameter called
projectId
. This means if we enter the project edit page via a URL
as opposed to a parent flow, we can pass in the project
Id as a parameter and it will be assigned to the
projectId
variable. This gives us two ways to enter a page but a
singular way to get the
projectId
in the flow that we use to get the project to edit. The
on-start
element is evaluated when we first start the flow. Here,
we call the
findProject()
method on the project Dao and pass in the
projectId
value. The result, which is the project we will be
editing, is put into the
project
variable in the flow scope. In our project edit page, we
can reference the
#{project}
variable and it will receive the value of our
project
variable defined in the web flow.
Example 2.7.
projectEdit.xhtml
page
<h:messages globalOnly="true" styleClass="message" />
<h:form>
<h:panelGrid columns="2">
Project Id : <h:outputText value="#{project.id}" />
Title : <h:inputText value="#{project.title}" />
</h:panelGrid>
<h:commandButton action="save" value="Save" />
<h:commandButton action="cancel" value="Cancel" />
</h:form>
Again, we have
save
and
cancel
actions which only have meaning if we look back at our
flow. The
save
transition calls the expression
projectDao.save(project)
which is used to save the project to the database. It
then transitions to the
save
end-state node which has a
commit
attribute set to true so the entity manager commits any
changes. The view for the
end-state
is set to redirect to a view outside of the flow. The
cancel
transition is virtually identical except that it doesn’t
call save changes or commit changes at the end state.
Note that we might run this flow from a parent flow, or
this flow might run as a top level flow. While this is
useful from the perspective of having re-usable code, it
may cause problems in determining when the right time to
commit the changes are. If this subflow is part of a
larger process, we don't want to commit right away, but
if it is also used as a process on its own, then there
may be good reasons to perform the commit here.
If we go to the projects page, we can click on the edit
link, and it will go to the project edit page. Clicking
cancel or save returns us to the project list page. If
we enter the URL
/projectEdit?projectId=2
, it will edit the second project. If we click save or
cancel, the flow will end and we will be redirected back
to the projects list page. One problem we do have is
returning messages regarding the success or failure of
our actions. The faces messages mechanism has been
abstracted through Spring Web Flow, although it can
still be used directly. Faces messages do not survive a
redirect which both Seam and Spring work around, however
Spring does not propagate faces messages outside of a
flow since it uses a flash scope. Let’s add a simple
message handler by using a message writer bean which is
defined as follows :
Example 2.8.
MessageWriter.java
helper class
public class MessageWriter {
public void info(MessageContext ctx,String message) {
ctx.addMessage(new MessageBuilder().info().defaultText(message).build());
}
public void error(MessageContext ctx,String message) {
ctx.addMessage(new MessageBuilder().error().defaultText(message).build());
}
public void savedChanges(MessageContext ctx) {
info(ctx, "Saved Changes");
}
public void cancelledChanges(MessageContext ctx) {
info(ctx, "Changes cancelled");
}
}
This is a really simple class to put messages into the flow. The MessageContext object that is passed in is the instance of the message context used by spring and is an abstraction of the faces message object. We add method calls to the flow to generate messages in response to certain actions.
Example 2.9. Adding expressions to generate messages on transitions.
<view-state id="projectEdit"> <transition on="save" to="save"> <evaluate expression="projectDao.save(project)" /> <evaluate expression="messageWriter.savedChanges(messageContext)"/> </transition> <transition on="cancel" to="cancel"> <evaluate expression="messageWriter.cancelledChanges(messageContext)"/> </transition> </view-state>
When we save or cancel the project, the message for the user is pushed into the message context for display on the next page. However, as stated earlier, the messages do not survive the end of a flow which means that those messages will not be displayed if you enter the projectEdit page as a top level workflow.
Now let’s add a flow for viewing the project, everything is almost the same, we use the same mechanism to get the projectId and load the project. The only real difference is that we have a transition to edit the project from the view page.
Example 2.10.
projectView.xml
web flow.
<input name="projectId" value="requestScope.projectId" /> <on-start> <evaluate expression="projectDao.findProject(projectId)" result="flowScope.project"> </evaluate> </on-start> <view-state id="projectView"> <transition on="close" to="close" /> <transition on="edit" to="edit"/> </view-state> <subflow-state id="edit" subflow="projectEdit"> <input name="projectId" value="project.id" /> <transition on="save" to="projectView"/> <transition on="cancel" to="projectView"/> </subflow-state> <end-state id="close" view="externalRedirect:/projects" />
From the view page, we can call the edit subflow to invoke the same edit subflow which brings re-use to our flows. Again, we just need to pass in the projectId value like we did from the projects page. Also, since we invoke the editor as a subflow, when we return, we return to the view flow which means it will display any messages that were generated from the edit page.
Now let’s consider the project issues. We want to
display the issues for each project on the view page in
a kind of master detail fashion. For now, we will take
the easy route and just connect the data table to the
#{project.issues}
value.
Example 2.11.
Listing issues in
projects.xhtml
<h:dataTable value="#{project.issues}" var="v_issue">
<h:column>
<h:outputText value="#{v_issue.id}" />
</h:column>
<h:column>
<h:commandLink value="#{v_issue.title}" action="view" />
(<h:commandLink value="edit" action="edit" />)
</h:column>
</h:dataTable>
If we open this page as-is, we will get an error because
of Lazy Initialization of the issues on the project
object. The reason for this is because the entity is
detached at the point of rendering the page. In order to
fix this, we simply add the
persistence-context
element at the top of our project view flow. If we
re-load the page, it works,however we have introduced
another problem. If you click the edit button from the
project view page, make changes and save it, you are
returned to the project view page, the saved changes
message appears but…the project that we edited has not
changed. Click the close button and go back to the
projects page. You should see the changes in the list of
projects, so the problem was local to the project view
page.
The problem is that we used the same persistence context
in the view flow. The project entity was loaded when we
first went to the project view page. We edited the
project, and returned to the project view page. When we
started the view flow, we searched for the project
entity based on the project Id and put it into
flowScope. When we return from the project edit page,
that value is still in flow scope and will be re-used.
To get around this, we would need to refresh the project
entity if the return value from the editor subflow is
save
.
Example 2.12. Refreshing the entity in a flow.
<subflow-state id="edit" subflow="projectEdit"> <input name="projectId" value="project.id" /> <transition on="save" to="projectView"> <evaluate expression="projectDao.refresh(project)" /> </transition> <transition on="cancel" to="projectView" /> </subflow-state>
This isn’t ideal, since you also have problems with
regards to refreshing the page. If you display the
project view page while changing the project data in
either another window or directly in the database, the
same values are displayed in the refreshed project view
page. To me, this is wrong as in general, in an entity
view, each refresh should literally refresh the data.
I’m sure there is a way around this using web flow that
I just haven’t come across. In theory, I should be able
to put the evaluation expression in an
on-render
tag in the project view page so the item is refreshed on
each page refresh. However, when I tried this, I got
errors since web flow had problems with knowing about or
seeing the
projectId
value.
Now let’s look at the view and edit pages for the issues. They are pretty much the same as the view and edit pages for the projects, except the names of parameters and variables, so I won’t reproduce the code here.
However, we do need to consider one issue. If we are to
display a list of issues for a project in the project
view page and allow the selection of that project, we
need to wrap it in a dataModel, which isn't done
currently since we are using
#{project.issues}
to get the list of issues. We have three choices here.
The first is to simply use a GET request passing the
issueId as a parameter. However, request would take us
out of any workflows we might be in. Alternatively, we
could wrap the current list of issues in a data model.
Example 2.13. Putting the list of issues into a flow as a dataModel.
<evaluate expression="project.issues"
result="flowScope.issues" result-type="dataModel">
Adding this to the start of the
projectView
flow wraps the list of issues from the project attribute
in a data model. Alternatively, we can create a separate
function which will allow us to get a list of issues
independently from the project entity. This is probably
a better solution since a project may end up with
thousands of issues which we will want to paginate
without having to load them all. However, using this
solution changes a number of things for us as we will
see. First off, we’ll implement an bean that has an
IssueDao
interface.
Example 2.14.
IssueDao
interface.
public interface IssueDao {
public List<Issue> findIssuesForProject(Long projectId);
public Issue findIssue(Long issueId);
public void refresh(Issue issue);
public void save(Issue issue);
}
I won’t include the implementation since it is fairly
straightforward, but we call it
IssueDaoBean
and add it as a spring bean called
IssueDao
.
At the top of our
projectView
flow, we add a call to find the issues for a project and
put the results into a flow scoped data model called
issues
.
Example 2.15. Putting the issues into a flow based on query results.
<evaluate expression="issueDao.findIssuesForProject(projectId)" result="flowScope.issues" result-type="dataModel"> </evaluate>
In our web page, where the dataTable was using
#{project.issues}
, we change it to just
#{issues}
.
Example 2.16.
Using our new
issues
variable in the data table.
<h:dataTable value="#{issues}" var="v_issue">
Now that we independently fetch the list of issues, we
can technically remove the
persistence-context
element. If you recall, we added this because we wanted
to fetch the list of issues in the rendering phase and
didn't want to cause a Lazy Initialization Exception.
Since we know we have already fetched the data we need
at the start of the flow, we no longer need it. However,
if we need to display the status entity of the issue, we
would need the same PC to be available on the rendering
phase assuming the status is fetched lazily. To solve
this, we have two options. Either keep the persistence
context for the duration of the flow, or explicitly load
the status entities when we get the list of issues in
the query which is a better option. However, for now,
let's keep the persistence context for the duration of
the flow. There are issues associated with unnecessarily
keeping a persistence context which we'll see in the
next section.
In Spring Web Flow, Persistence Contexts (PCs) can last the duration of the flow, or for the duration of the request. It is possible to have a PC scoped to last for a conversation but this is not yet implemented. Whether you use the long or short PC scopes, they both introduce different problems. Note that some of these same problems apply to Seam also where a PC can be conversational or non-conversational depending on whether a long running conversation is active or not.
Short PC scopes can result in detached entities that need re-attaching when persisting changes. The problem with this is that you lose optimistic locking since the version you started out with could be different than the version you ultimately save with if another user has modified the entity.
While it may seem tempting to always use a longer PC scope, such as one scoped to the flow, there are different problems associated with it. For example, in a view page with a flow scoped PC, when you refresh the page, an attempt is made to refresh the data you are viewing. However, because you are using a flow scoped PC, the same instance of the data is returned from the PC. This means that you would manually need to trigger a refresh of the data by calling the refresh method on the persistence context.
So, now we have our issue edit/view pages up and
running, let's look through a couple of scenarios
that we might encounter. We can remove the
persistence-context
element from the issue view flow since we don't need
to keep our PC around. If we try and edit the issue
and save it, when we get back to the
issueView
flow, we run into a problem. When the outcome was
save
, we refreshed the
issue
entity to get the latest version using the variable
in the flow.
Example 2.17.
Refreshing the entity on a
save
action.
<subflow-state id="edit" subflow="issueEdit"> <input name="issueId" value="issue.id" /> <transition on="save" to="issueView"> <evaluate expression="issueDao.refresh(issue)"/> </transition> <transition on="cancel" to="issueView" /> </subflow-state>
Because we are not using the same persistence context for the duration of the flow, we get an 'Entity not managed' exception because we are refreshing the entity against a different PC. We can solve this a few ways. One is to change the call to refresh to a call to find the issue.
Example 2.18. Refreshing the issue by re-loading it as a new instance.
<evaluate expression="issueDao.findIssue(issue.id)" result="flowScope.issue">
This calls the find method using the current PC and
puts the value in the
issue
variable in the flow scope. Another way is to write
a smarter refresh method in the Dao. We can't just
call
find()
since we will get the stale version back, so we
check if it is in the PC and if so, we refresh it.
Otherwise we call the
find()
method to get an instance back.
Example 2.19. Refreshing / loading the entity.
public Issue refresh(Issue issue) {
if (entityManager.contains(issue)) {
entityManager.refresh(issue);
return issue;
} else {
return findIssue(issue.getId());
}
}
This way, if the issue is not part of the current
PC, then it loads a new one using the ID of the old
one. If it is a part of the PC, then it simply
refreshes it. The returned value is pushed into the
flow scope
issue
variable.
Example 2.20. Refreshing the entity and putting it into the flow
<evaluate expression="issueDao.refresh(issue)" result="flowScope.issue">
The advantage of this method is that it will work
whether you are using flow scoped persistence
contexts or not and you can change between the two
without requiring any additional changes (toggling
between the
find()
and
refresh()
methods on the
issueDao
).
Incidentally, this kind of feature can also be used
in the
projectView
page where we list the issues. When the user views
or edits an issue we need to refresh the issue in
the list on the project page since it may have
changed (the user may click view issue, then edit
the issue, and then end up back to the project view
page). To ensure that we refresh the issue, we can
add an
on-exit
element to the
issueView
and
issueEdit
states so that we can refresh the item in the issue
list.
Example 2.21.
Flow controlled refresh of the entity on the
on-exit
phase in
projectView
flow.
<subflow-state id="issueView" subflow="issueView"> <input name="issueId" value="issues.selectedRow.id" /> <transition on="save" to="projectView" /> <transition on="close" to="projectView" /> <on-exit> <evaluate expression="issueDao.refresh(issues.selectedRow)"> </evaluate> </on-exit> </subflow-state>
This works great, when the user views an issue and we come back to the project view flow, just that one selected issue is refreshed.
One nice aspect of Spring Web Flow is the amount of control you can have over the data in your flows, the scope of it, and the scope of the persistence contexts. Also, everything is local to the flow itself, the data is declared in the flow, and inserted into a scope according to the flow definition. This means your flows can be strongly decoupled from each other, and also from your code since most code is stateless. State is handled by declaring stateful variables in the flow itself.
I can imagine situations where this will result in problems though if careful variable naming is not used. The fact that variables can be declared as flow scoped as opposed to conversationally scoped will reduce these incidents if users can limit the scope to the flow
Let's look at one more scenario that is a little more
complex. Let's say that as part of editing an issue we
want to select a different project for the issue. For
this, we can re-use the project list page, but this time
the list will have a button to select the project for
the issue we are editing. When a project is selected, we
should come straight back to the issue editing page, and
the project should be selected as the issue's project.
It is feasable that such a listing page would be used in
a number of places throughout the application making the
re-use of this page important. We don't want any hard
coding or hacks in the page to indicate where to
navigate to next, we want to use the page flow functions
to control our navigation. The other aspect of this
problem is that we need to pass the selected project
back to the issue edit flow.To start with, we'll add a
selectProject
transition to our
issueEdit
flow.
Example 2.22. Transition to navigate to the project selection page.
<view-state id="issueEdit"> … … <transition on="selectProject" to="selectProject" /> … … </view-state>
This transitions to our select project sub flow which is
the
projects
flow we created at the beginning. Again, the goal here
is to re-use elements we have already built. This flow
state is also responsible for any changes to our issue
entity due to the project selection.
Example 2.23. The project selection state in the edit issue flow.
<subflow-state id="selectProject" subflow="projects"> <input name="doSelect" value="true" type="java.lang.Boolean" /> <transition on="selectProject" to="issueEdit"> <set name="issue.project" value="currentEvent.selectedProject"> </set> </transition> <transition on="selectCancel" to="issueEdit" /> </subflow-state>
As of Web Flow 2.0.1,
currentEvent.selectedProject
should be replaced by
currentEvent.attributes.selectedProject
due to changes in how SWF accesses the attributes in
the current event.
What we are doing is calling the
projects
flow and pushing a variable called
doSelect
in and setting it to
true
. If the subflow returns
selectProject
then we expect the subflow to pass out a value called
selectedProject
which we assign to the
issue.project
value.
In our
projects.xhtml
page, we add a new column to the list of projects to
contain the button to select a project. This column
should only be rendered if the
doSelect
flag is set to true. This flag is only set to true when
we are calling the flow from another flow for the
purpose of project selection.
Example 2.24.
projects.xhtml
column containing a link for selecting a project
<h:column rendered="#{doSelect == true}">
<f:facet name="header">Select</f:facet>
<h:commandLink value="Select" action="selectProject"/>
</h:column>
At the bottom of the table we add another button to
cancel the selection if the user decides not to change
the project.Again, rendering this button depends on the
doSelect
flag being set.
Example 2.25.
projects.xhtml
button for cancelling project selection.
<h:commandButton rendered="#{doSelect == true}" value="Cancel" action="selectCancel"/>
The select and cancel buttons returns the actions
selectProject
and
selectCancel
respectively. These actions are handled by the
projects
page flow. As a part of selection process, we need to
pass the selected project out of the flow and into the
parent flow. We do this using a new
end-state
which outputs the selected row. We also have an end
state for the cancel action which does not output the
selected project.
Example 2.26.
End states in
projects.xml
flow for selecting or cancelling project selection.
<end-state id="selectProject"> <output name="selectedProject" value="projects.selectedRow" /> </end-state> <end-state id="selectCancel" />
If the user cancels the selection, then we just go to
the end state, but if they click select, then we pass
back the
projects.selectedRow
value in a variable called
selectedProject
.
What is really great about this is I can start any number of flows and they will manage themselves correctly. I can navigate the pages in any order by editing and viewing projects or issues, changing the project for an issue but editing the project before selecting it. As I save and close my way back down the flow stack everything works perfectly. My data is isolated between different conversation and I don't worry about overwriting values.
Unfortunately, there is no provision for using a drop down in Jsf without resorting to manually handling selectItems and taking responsibility for mapping the selected item to a scoped list within the flow.
Let's look at the task of creating a new project and a new issue. For this, we need to determine that the project or issue Id is blank, and thus create a new one. I found this to be a somewhat thorny problem to a degree. The most obvious way was to use the findProject(projectId) to return a project, or a new project if projectId is null or 0. Note that this is a quick solution and you may want to use an alternative mechanism for production. Especially since security authorization will be involved in most cases to determine whether the user can view or create the item in question.
Example 2.27.
Code in
projectDao
to find or create a project.
public Project find(Long projectId) {
if (projectId == null || projectId == 0) {
return new Project();
} else {
return entityManager.find(Project.class, projectId);
}
}
Now we just add a "new" button on the project list and make it call the sub flow for editing the project
Example 2.28.
Transition in
projects.xml
for creating a new project.
<view-state id="projects"> … … … <transition on="new" to="projectNew" /> … … … </view-state> <subflow-state id="projectNew" subflow="projectEdit"> <transition on="cancel" to="projects" /> <transition on="save" to="projects" /> </subflow-state>
Note that we do not pass a projectId in this time. If we run this, we can click new, edit a new project and save it. Note also that this fits in with our existing project edit workflow without needing any changes.
I can view an existing project, edit one of the issues, and decide to change the project. Rather than pick an existing project, I can create a new project, save it, and then select it as the project for the issue I'm editing. Also, I could cancel the changes to my issue and the project will still be saved. The beauty of it is that since the flows are self contained, data is isolated and I don't have to worry about variable names overlapping or the flows becoming intertwined. It just works without any changes to the other flows.
For issues, we have a tougher challenge. If the issue Id
is null, we cannot just create a new issue, we need to
have a project to attach it to. We add a new method to
the
issueDao
called
findOrCreateIssue
into which we pass the
issueId
and any
projectId
we have.
Example 2.29.
issueDaoBean.java
method to find or create an issue.
public Issue findOrCreateIssue(Long issueId, Long projectId) {
if (issueId == null || issueId == 0) {
if (projectId == null || projectId == 0) {
//error!
return null;
} else {
Issue issue = new Issue();
issue.setProject(entityManager.find(Project.class, projectId));
return issue;
}
} else {
return findIssue(issueId);
}
}
This should take care of creating a new issue or project when we don't pass an Id in. There is the question of security and exception handling which I'll get to later.
For now, let's continue with the problem of adding issues. In the project view page, we add a button to add a new issue to the project.
Example 2.30.
Button in
projectView.xhtml
to create a new issue.
<h:commandButton action="issueNew" value="New Issue" />
In the
projectview
flow, we add a transition to the view for creating new
issues.
Example 2.31.
Transition in
projectView.xml
flow to create a new issue.
<transition on="issueNew" to="issueNew" />
This goes to a subflow for editing issues. As part of
this subflow, we pass in the value
project.id
as the ID of the project we want the new issue for.
Example 2.32. Subflow state for creating a new issue for the project
<subflow-state id="issueNew" subflow="issueEdit"> <input name="projectId" value="project.id" /> <transition on="save" to="projectView" /> <transition on="cancel" to="projectView" /> <on-exit> <evaluate expression="issueDao.findIssuesForProject(project.id)" result="flowScope.issues" result-type="dataModel"> </evaluate> </on-exit> </subflow-state>
When we return and exit that subflow, we refresh the list of issues for the project. Again, the level of de-coupling between the two flows is excellent, we pass in what we need to and we check for the responses we expect.
At this point, it looks like we’ve completed the demo application. While the example is not that complex, it should give you an idea of how to write a stateful CRUD application with Spring and Spring Web Flow.
In our application, when an error is reached, we should be throwing an exception which should then be caught and our flow redirected, and/or an error message displayed. In particular I’m thinking of the cases where we want to load an issue or project, and there is no Id, or the item for that Id does not exist anymore. There may also be cases where the user may be creating a new item when they don't have security rights to do so. There exists mechanisms for handling exceptions which lets us write exception handling classes to handle the exception and if possible add messages to be displayed on the current view.
However, we call these functions on the start of the
flow at which point, the view state is not available to
the flow, and the faces context is not available,
therefore if we do find an exception, there isn’t much
we can do about it. We could make our
findOrCreateIssue()
call in the
on-render
phase of a state, but we have problems there since it
cannot see the
projectId
or
issueId
values properly. It seems like these values have a very
short scope.
Overall, the exception handling seems limited (almost non-existent) without writing custom exception handlers for the view. Again, this is done through the bean mechanism so there is plenty of chance to write handlers that uses lists of re-usable handlers to process exceptions. While this does offer a flexible system, it would be nice to see some default in there for error handling, even if it is just staying on the page, and pushing the exception message into the messageContext. I think though that there are more complications involved in that though. The help documents and examples don’t cover exception handling very well, but I’m sure we’ll see more on the topic from Spring.
Spring Web Flow does come with a number of ajax related JSF components that can allow you to limit the areas that are re-rendered on submission which is quite nice. It also provides some decorations for existing DOM nodes that can apply client side validation which is also nice. JSF has a number of existing Ajax frameworks out there that can achieve some of this, however they may be considered heavyweight and loaded with unneeded components, so Spring's Ajax controls offer a lighter alternative if you wanted to apply a simple ajax solution to your application.
Spring Security can also be used with flows and you can apply authentication at the flow, view and even transition levels.
Flows also have an inheritance mechanism which allows you to let one flow inherit from another. While the inheritance is a little less flexible than object inheritance, this is still a great feature for some of those views that are often used. This could easily make up for some of those cases where the data factory methods needs to be repeated in similar flows. For example, the project view and edit flows both need to grab an instance of an object based on the project Id parameter.
IDE support is missing in some areas, although mostly in the non-Spring areas. The JSF page editor is missing the ability to auto complete code for beans, and there are places where I expected auto-completion in the flow editors, but didn’t get it. The web flow editor in this version was still thinking in terms of Web Flow 1.0, although it was still able to come up with diagrams for my flows. Auto completion for the Spring elements worked great, from specifying beans to writing flows and auto-completing the list of states available to transition to. For me, not having auto complete in the JSF editor is a pain.
Spring Web Flow offers some flexibility with the
Persistence Context. The
Persistence-Context
element at the start of flows lets us define whether the
PC is event, flow or conversation scoped. Conversation
scoped is not currently implemented. This could cause
some problems if you are pushing entities from one flow
to another since they could have different PCs and the
entity would be detached in the new flow.
Overall, this is a great framework. Version 2.0 included a heavy re-write to make it work much more amicably with JSF and it shows. The flow language is clean and straightforward, although it does tend towards some repetition. Having default transitions might be a nice addition to both ease repetition and also avert disaster in the event of changes to a subflow which returns an unexpected value to a parent flow. It is great how easily the flows can be written so you can take a navigation path that is an endless circle, and still come back out the way you came in without data getting overwritten. The only other criticism is that it does make a mess of your URLs by adding large flow state values in there. As a newly re-written framework, there are plenty of places where the documentation is lacking, especially for newcomers.
The data management and scoping is really nice, the localization of the definition to the flows is great, even if it is at the expense of defining things multiple times for similar flows. The IoC and Dependency Injection pieces of this solution probably need no introduction as they are provided by the Spring core.
| http://www.andygibson.net/ | Copyright © 2008 Andy Gibson |