At line 1 added 166 lines |
!!! Let's hear that one more time - this time in Java |
|
Having inherited a working XML-driven app at the end of the last page, we will show how straightforward it is to move over to pure Java view producers. The app we produce here functions identically to the {{category}} version we just developed. |
|
! Why use Java producers? |
|
In general, Java view producers will be considerably more efficient than the XML equivalents - not only because of losing the overhead of parsing XML files (actually these are cached at application scope anyway - but there is some overhead in cloning their object content), but also because Java producers can take a number of "shortcuts" in constructing the component tree. The most important shortcut is avoiding issuing EL bindings for components that will generate no input. A Java producer will generally have the app's data model right in its hands, and it is much quicker to fish out and render the relevant values during production, rather than wait for this to happen during fixup - mainly since there is one layer less of reflection. |
|
Another important time saver is that view logic is now "actual" logic - where in the XML world we had Replicators and Switches, we now have loops and {{if}} statements. |
|
But basically, Java producers are just more idiomatic, and make full use of the available tool chain. XML component production is really only intended for "autogenerated" apps (perhaps via XSLs), or perhaps environments where apps may frequently be "hot-edited". |
|
! Changes |
|
In many frameworks, the change from XML view definitions to Java code might be a lot of upheaval, but in RSF you will see that the great majority of our app survives untouched. Naturally we will continue using exactly the same HTML templates, and also the same auto-generated data model. Most of the same beans will be defined in Spring, with only the extra beans required to define our views. An interesting insulating feature of RSF is that, despite being a Hibernate-driven app, our Java code will *still* contain no reference to Hibernate classes - this is the sort of benefit that Spring-enabled apps tend to enjoy, and RSF is careful to preserve it. |
|
! Build structure |
Since this is the "definitive" version of the app, we have given it a fully mature build structure separating it into three separate Maven artefacts. Firstly, [category-datamodel|https://saffron.caret.cam.ac.uk/svn/projects/RSFHibernateCookbook/trunk/category-datamodel/] defines ''just'' the autogenerated Hibernate datamodel and the Hbm2Java task that builds it. In complex environments this artefact (producing a JAR) may need to be deployed to a different target than the webapp. |
|
Secondly, there is the "main code" for the application, defined as a [partial WAR|PartialWAR] in [javacategory-code|https://saffron.caret.cam.ac.uk/svn/projects/RSFHibernateCookbook/trunk/javacategory-code/]. This defines all the logic of the application, and produces a WAR artefact that just needs to be livened up by the addition of some JARs to lib and the web.xml. |
|
These missing elements are held in the third component, [javacategory-webapp|https://saffron.caret.cam.ac.uk/svn/projects/RSFHibernateCookbook/trunk/javacategory-webapp/]. This is a slimline artefact that contains the main deploy task and packaging. Just issue {{maven}} from this directory to execute the top-level build. |
|
All of these artefacts may easily be checked out from SVN by using the [rsf-samples|https://saffron.caret.cam.ac.uk/svn/bundles/rsf-samples/] external, described on the main downloads page. |
|
|
NB - to allow Maven to compile both app and auto-generated source trees in the same project, we need a bit of hackery - please consult the [maven.xml|https://saffron.caret.cam.ac.uk/svn/projects/RSFHibernateCookbook/trunk/javacategory-webapp/maven.xml] for details. In production you would split an app into 2 or more Maven projects to make your builds more coherent. |
|
! Recipes listing |
|
The recipes listing view {{[recipes|https://saffron.caret.cam.ac.uk/svn/projects/RSFHibernateCookbook/trunk/javacategory-code/src/java/uk/org/ponder/rsf/cookbook/producers/Recipes.java]}} is one of the most complicated views, and also shows a few contrasts with its XML counterpart - here is the full source minus imports: |
|
{{{ |
public class Recipes implements ViewComponentProducer { |
public static final String VIEW_ID = "recipes"; |
private List recipelist; |
public String getViewID() { |
return VIEW_ID; |
} |
|
public void setRecipeList(List recipelist) { |
this.recipelist = recipelist; |
} |
|
public void fillComponents(UIContainer tofill, ViewParameters viewparams, |
ComponentChecker checker) { |
UIForm form = UIForm.make(tofill, "basic-form"); |
for (int i = 0; i < recipelist.size(); ++ i) { |
Recipe recipe = (Recipe) recipelist.get(i); |
String id = recipe.getId().toString(); |
UIBranchContainer reciperow = UIBranchContainer.make(form, "recipe-row:", id); |
UIOutput.make(reciperow, "confirm-title", recipe.getTitle()); |
UIInternalLink.make(reciperow, "recipe-show", recipe.getTitle(), |
new EntityCentredViewParameters(RecipeShow.VIEW_ID, |
new EntityID("Recipe", id), null)); |
Category category = recipe.getCategory(); |
UIOutput.make(reciperow, "recipe-category", category == null? "" : category.getName()); |
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); |
UIOutput.make(reciperow, "recipe-date", sdf.format(recipe.getDate())); |
UICommand destroy = UICommand.make(reciperow, "recipe-destroy"); |
destroy.parameters.add(new UIDeletionBinding("#{Recipe." + id +"}")); |
} |
UIInternalLink.make(tofill, "recipe-create", |
new EntityCentredViewParameters(RecipeEdit.VIEW_ID, new EntityID("Recipe", "new 1"), |
EntityCentredViewParameters.MODE_NEW)); |
UIInternalLink.make(tofill, "index", new SimpleViewParameters(Index.VIEW_ID)); |
} |
|
} |
}}} |
|
! Entity injection |
|
The first thing to notice is that this is clearly a [request scope view producer|RequestScopeViewProducers] - observe that the list of recipes it works on has been ''injected'' into it using RSF's request-scope Spring clone, [RSAC]: {{public void setRecipeList(List recipelist) }}. In fact this dependency has been resolved to exactly the same HQLQuery bean that we used in the XML view - this sort of capability not only keeps our code free of Hibernate dependence, but also saves us from having to write a tedious "DAO"/"Service" layer for our app. |
|
! Direct Java |
|
Secondly, we see that each line of the recipe table is built up by a good old-fashioned {{for}} loop iterating over the list. This in general is much faster than the XML equivalent using the Replicator. |
|
Further interesting contrasts with {{[recipes.xml|https://saffron.caret.cam.ac.uk/svn/projects/RSFHibernateCookbook/trunk/xmlcategory/src/webapp/WEB-INF/producers/recipes.xml]}} are i) the direct use of calls like {{recipe.getTitle()}} and {{category.getName()}} instead of issuing ELs for deferred evaluation, and ii) the use of a Java standard "SimpleDateFormat" in place of the date rendering {{resolver}} we had to use in XML - these are the sorts of "back to basics" benefit that becomes more and more important with larger and more complex ViewProducers. |
|
However, in its functional effects this producer is one-for-one identical with its XML counterpart - exactly the same tree of components is produced (with a few missing EL references where Java can achieve the same effect directly). |
|
! Static {{VIEW_ID}}s |
|
A final thing to note is the use of the static field {{VIEW_ID}} to encode the view of this producer. This is a good victory for "Don't repeat yourself" semantics since it enables ViewProducers to issue internal links to one another in a type-safe way (e.g. {{RecipeEdit.VIEW_ID}}). Given our ''complete'' separation of presentation from content in RSF, we cannot avoid mentioning the same strings in HTML as we do in code, but at least once we are within code, each view id appears in just one place. |
|
! Similarities |
|
The other "complex" view in our app is the recipe editing view - not only does it include an entity selection control, but also defines some navigation semantics: |
|
{{{ |
public class RecipeEdit implements ViewComponentProducer, |
NavigationCaseReporter, ViewParamsReporter { |
public static final String VIEW_ID = "recipe-edit"; |
|
public String getViewID() { |
return VIEW_ID; |
} |
|
public void fillComponents(UIContainer tofill, ViewParameters viewparams, |
ComponentChecker checker) { |
EntityCentredViewParameters ecvp = (EntityCentredViewParameters) viewparams; |
UIOutput.make(tofill, ecvp.mode |
.equals(EntityCentredViewParameters.MODE_NEW) ? "new-recipe-heading" |
: "edit-recipe-heading"); |
UIForm form = UIForm.make(tofill, "basic-form"); |
UIInput.make(form, "recipe-title", "#{" + ecvp.getELPath() + ".title}"); |
UIInput.make(form, "recipe-description", "#{" + ecvp.getELPath() |
+ ".description}"); |
UISelect category = UISelect.make(form, "recipe-category"); |
category.optionlist = UIOutputMany.make("#{categories-all}", |
"#{fieldGetter.id}"); |
category.optionnames = UIOutputMany.make("#{categories-all}", |
"#{fieldGetter.name}"); |
category.selection = new UIInput(); |
category.selection.valuebinding = new ELReference("#{" + ecvp.getELPath() |
+ ".category.id}"); |
category.selection.darreshaper = new ELReference("#{id-defunnel}"); |
UIInput.make(form, "recipe-instructions", "#{" + ecvp.getELPath() |
+ ".instructions}"); |
UIInternalLink.make(form, "recipe-show", new EntityCentredViewParameters( |
RecipeShow.VIEW_ID, ecvp.entity)); |
form.parameters.add(new UIELBinding("#{" + ecvp.getELPath() + ".date}", |
new Date())); |
UICommand.make(form, "recipe-save"); |
UIInternalLink.make(form, "recipe-list", new SimpleViewParameters( |
Recipes.VIEW_ID)); |
} |
|
public List reportNavigationCases() { |
List togo = new ArrayList(); |
togo.add(new NavigationCase(null, new SimpleViewParameters(Recipes.VIEW_ID))); |
return togo; |
} |
|
public ViewParameters getViewParameters() { |
return new EntityCentredViewParameters(VIEW_ID, new EntityID("Recipe", null)); |
} |
|
} |
}}} |
|
This view marks itself as requiring non-default ViewParameters by implementing the {{ViewParamsReporter}} interface - this returns the same "exemplar" object (of type EntityCentredViewParameters) as was encoded at the end of the [XML view|https://saffron.caret.cam.ac.uk/svn/projects/RSFHibernateCookbook/trunk/category/src/webapp/WEB-INF/producers/recipe-edit.xml]. |
|
Similarly, it registers "JSF-style" navigation rules for itself by implementing {{NavigationCaseReporter}}, again returning the same rule list. as was encoded in the {{navigation-case}} XML tag. Unlike JSF navigation rules which are entered in the global {{faces.config}} file, RSF's treatment allows views to be more modular. |
|
What's quite interesting is the definition of the selection control named {{category}} - while we would have the power in Java to construct the "final form" {{String[[]}} representing the option labels and IDs in code, the code for this would be quite tedious. In a more functional language than Java this would be easy to achieve using transforms and maps, but here it actually turns out much more concise to let RSF's EL system to do the legwork for us - we actually issue the exact same [UISelect] object complete with ELs as we do in XML. |
|
!!Conclusion |
|
This Java app is somewhat more concise than the XML version, will perform considerably better, and is ready for scaling up to arbitrary complexity and horsepower thanks to the "heavy muscle" of Spring and Hibernate behind the scenes (the latter as far behind the scenes as we can make it). We cannot boast equal concision to the Ruby version, which is advertised at the end of the tutorial as weighing in at 47 lines of Ruby (we have around 3x that quantity of Java code) - but are probably doing a lot better than any comparable Java framework. A lot of the verbosity is a direct consequence of achieving the ''complete'' presentation/logic separation "holy grail" - having separated these two we must go to the trouble of matching them up again (with {{rsf:id}}s). While this involves more typing, the coherence and stability of our projects will benefit enormously in the long-term. One aspect of this is that our app already has much "longer legs" than the Ruby equivalent - it is ready to leap into strange hosting environments (portals or other proxies) and different UI technologies with which a scripted app with mixed style and logic would suffer. |
|
Some more of the verbosity is sadly simply inherent in Java - accessor methods, constructor calls, and package-qualified names. Luckily in the environment of modern tools such as Eclipse, this extra text rarely translates to extra ''typing''. |
|
Having looked through these 3 tutorials, you have seen pretty much all of RSF. One important feature of RSF is that it is very ''loosely bundled'' - looking through its Spring files, you will see that there is no particularly solid distinction between "framework" components and "user" components, which makes it an ideal integration platform as well as making it unprecedentedly easy to adapt to different ways of working. I hope that its users will enter a more close relationship with the source base than is usual (easier since the code base remains pretty compact), and look forward to all sorts of interesting and productive components being designed and swapped across the community. |
|
I thank you! |
|
---- |
Head - [Hibernate Cookbook|HibernateCookBook]\\ |
Page 1 - [Query beans, templates and views|HibernateCookBook_1]\\ |
Page 2 - [Request scope bindings and error handling|HibernateCookBook_2]\\ |
Page 3 - [The main app - entity selectors and Javascript|HibernateCookBook_3]\\ |
Page 4 - Let's hear that one more time - this time in Java\\ |
---- |