Implementing the XForms-JSF shopping cart
Implementing the NameValuePair, Category, and Product classes
In Components of the XForms-JSF shopping cart application, we discussed three JSF tags:
xcart:category, xcart:product, and xcart:cart.
These tags form the XForms-based
shopping cart tag library. We wish to make this tag library independent of our
shopping cart application to ensure that you can integrate our
shopping cart tag library into your own shopping cart application.
To make our shopping cart tag library independent
of the shopping cart application, we need to implement three
classes: NameValuePair, Category, and
Product. These classes will be part of the shopping
cart tag library. Application-specific model beans
initialize and use these classes.
The NameValuePair class just wraps a name-value
pair. The Category class holds the subcategories and
products corresponding to a particular category. The Product class
holds the details of an individual product (name, ID, description, price, etc.).
Let's look at the implementation details of the NameValuePair,
Category, and Product classes in detail.
The NameValuePair class contains two fields -- name and
value -- and their respective setter and getter methods. The
following code shows the implementation of the NameValuePair class:
public class NameValuePair{
protected String name;
protected String value;
public void setName(String name) {
this.name = name;
}
public String getName(){
return name;
}
public void setValue(String value){
this.value = value;
}
public String getValue(){
return value;
}
}//NameValuePair
|
Many of the classes in our shopping cart tag library and shopping
cart application use the NameValuePair class.
Any application that wants its data to be rendered using
the xcart:category tag instantiates a
Category object and populates it with application-specific data.
The xcart:category component uses this Category
object to fetch the application-specific data.
The Category class holds the category data in
id, products, ancestors, and
subcategories properties.
The id property of the Category class is a
String object that holds the ID of the current category.
The products, ancestors, and subCategories
properties are arrays of NameValuePair objects. The
products property contains the information of each product
in the category. The ancestors property tracks the ancestors of the current
category. The subCategories property stores the list of subcategories
in the current category.
The following code shows the implementation of the Category
class:
public class Category{
protected String id;
protected NameValuePair[] products;
protected NameValuePair[] ancestors;
protected NameValuePair[] subCategories;
public void setAncestors(NameValuePair[] nameId) {
ancestors = nameId;
}
public NameValuePair[] getAncestors(){
return ancestors;
}
public void setSubCategories(NameValuePair[] nameId) {
subCategories = nameId;
}
public NameValuePair[] getSubCategories(){
return subCategories;
}
public void setProducts (NameValuePair[] nameId) {
products = nameId;
}
public NameValuePair[] getProducts(){
return products;
} public void setId(String id) {
this.id = id;
}
public String getId(){
return id;
}
}//Category
|
Now let's discuss the implementation of the Product
class.
The Product class holds the product data in seven
properties:
-
id: Stores the ID of a particular product the user clicked in the catalog view
-
name: Stores the name of the product
-
price: Stores the price of the product
-
description: Stores the description of the product
-
features: Stores the features of a product in name-value pair form
-
optional-features: Stores the optional features of the product
-
selectedOptionalFeatures: Stores those optional features the user selected in the product specification view
The following code shows the implementation of the ProductData
class:
public class Product{
protected String id;
protected String name;
protected String price;
protected String[] options;
protected String description;
protected NameValuePair[] features;
protected NameValuePair[] optionalFeatures;
protected NameValuePair[] selectedOptionalFeatures;
public void setName ( String name) {
this.name = name;
}
public String getName (){
return name;
}
public void setId (String id) {
this.id = id;
}
public String getId(){
return id;
}
public void setDescription ( String description) {
this.description = description;
}
public String getDescription (){
return description;
}
public void setPrice( String price) {
this.price = price;
}
public String getPrice(){
return price;
}
public void setFeatures(NameValuePair[] features) {
this.features = features;
}
public NameValuePair[] getFeatures (){
return features;
}
public void setSelectedOptionalFeatures(NameValuePair[]
selOptFeatures){
this.selectedOptionalFeatures = selectedOptionalFeatures;
}
public NameValuePair[] getSelectedOptionalFeatures(){
return selectedOptionalFeatures;
}
public void setOptionalFeatures (NameValuePair[] optionalFeatures){
this.optionalFeatures = optionalFeatures;
}
public NameValuePair[] getOptionalFeatures (){
return optionalFeatures;
}
public void setOptions (String[] options) {
this.options = options;
}
public String[] getOptions(){
return options;
}
}//Product
|
Now let's start implementing our sample XForms-JSF
shopping cart. We have to implement three JSP pages (catalogView.jsp,
productView.jsp, and cartView.jsp), three JSF components
(UICategory,
UIProduct, and UICart), two event handler
classes (UICategoryActionListener and
UICartActionListener), and three model beans
(CategoryData, ProductData,
and CartData).
The catalogView.jsp page, UICategory component,
UICategoryActionListener event handler, and CartData
model bean form a set. We will explain the working of this set in
the next four sections. For the rest of the JSP pages, components, event handlers,
and model beans, we will only explain the differences from the first set.
Using the xcart:category tag to generate the catalog view
Look at the following catalogView.jsp page, which generates the catalog view:
<?xml version="1.0" encoding="iso-8859-1"?>
<html
xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:xforms="http://www.w3.org/2002/xforms">
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://afictitiousshoppingcart.com/XCart"
prefix="xcart" %>
<%@ taglib uri="http://afictitiousshoppingcart.com/XForms-JSF"
prefix="xforms-jsf" %>
<f:view>
<head>
<xforms-jsf:model value="#{categoryData.model" />
</head>
<body>
<xcart:category value="#{categoryData.category}"
action="#{categoryData.getAction}">
<f:action_listener type="model.UICategoryActionListener"/>
</xcart:category>
<xforms-jsf:commandButton label="Show Cart" immediate="true"
action="#{categoryData.getAction}"
actionListener="#{categoryData.showCartView}">
</xforms-jsf:commandButton> </body>
</body>
</f:view>
</html>
|
Note the following points:
- We have used the
xforms-jsf:modeltag from the XForms-JSF tag library in theheadelement. We explained this tag in Implementing the xforms-jsf:model component.
- We have included the
xcart:categorytag in thebodyelement, which contains two attributes:valueandaction.
- The JSP author provides the
xcart:categorytag with avalueattribute. Thevalueattribute contains the property of the model bean (categoryData.category), which contains the application data associated with the catalog view. ThecategoryData.categoryproperty is an object of theCategoryclass explained earlier. Note that the model bean (categoryData) can be an object of any class, but the property that contains the application data should be an object of theCategoryclass.
- The method of the model bean specified in the
actionattribute ("categoryData.getAction") controls navigation. In Navigation process, we explained how navigation works and how the JSF framework uses theactionattribute for navigation.
- We have associated
an action listener (
UICategoryActionListener) with thexcart:categorycomponent. When the user clicks any category or product in the catalog view, theUICategoryActionListenerclass handles the generated action event, which we will discuss in Implementing the UICategoryActionListener class.
- We have used the
xforms-jsf:commandButtontag to show the cart view. TheactionListenerattribute of thexforms-jsf:commandButtontag refers to the event handling method of the model bean. When the user clicks this button, the JSF framework calls this method to handle the generated action event.
Implementing the UICategory component
The xcart:category tag renders the list of categories
and products available in the catalog view. Each category or product is
rendered as a button. If the user clicks on any category, its
subcategories and products are displayed. If the user
clicks on a product, the product-specification view appears to show the
details of the product.
The first step toward the development of any JSF tag is
to make an entry in the TLD file. The TLD entry for the
xcart:category tag looks like the following code:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<taglib>
......
<tag>
<name>category</name>
<tag-class>xcart.CategoryTag</tag-class>
<attribute>
<name>value</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>id</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>action</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>binding</name>
<required>false</required>
<rtexprvalue> false </rtexprvalue>
</attribute>
<attribute>
<name>actionListener</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>immediate</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>rendered</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
.....
<!--other tag instances-->
<taglib>
|
In the above TLD entry, the name
element inside the tag element wraps the name of the JSF tag
(category), and the tag-class element wraps the complete qualified name of
the tag handler class (xcart.CategoryTag).
The xcart:category tag contains two attributes:
value and action. The two attribute elements
in the above TLD entry correspond to these two attributes.
Here's how we implement the xcart.CategoryTag
tag handler class:
public class CategoryTag extends UIComponentTag {
private String value;
private String action;
private String immediate;
private String actionListener;
public void setValue(Object value){
this.value = value;
}
public Object getValue(){
return this.value;
}
public void setAction(Object action) {
this.action = action;
}
public Object getAction(){
return this.action;
}
public String getComponentType(){
return "category";
}
public String getRendererType(){
return null;
}
public String getImmediate(){
return immediate;
}
public void setImmediate(String newImmediate) {
immediate = newImmediate;
}
public String getActionListener(){
return actionListener;
}
public void setActionListener(String newActionListener) {
actionListener = newActionListener;
}
public void setProperties(UIComponent component) {
super.setProperties(component);
UICategory catComp = (UICategory)component;
FacesContext fc = FacesContext.getCurrentInstance();
Application app = fc.getApplication();
if(action != null) {
if(UIComponentTag.isValueReference(action)) {
MethodBinding mb = app.createMethodBinding(action, null);
catComp.setAction(mb);
}
else{
MethodBinding mb = new ConstantMethodBinding(action);
catComp.setAction(mb);
}
}
if(value != null) {
if(UIComponentTag.isValueReference(value))
{
ValueBinding vb = app.createValueBinding(value);
catComp.setValueBinding("value", vb);
}
else
catComp.setValue(value);
}
if(actionListener != null) {
if(UIComponentTag.isValueReference(actionListener)) {
Class args[] = {
javax.faces.event.ActionEvent.class
};
MethodBinding mb =
app.createMethodBinding(actionListener, args);
catComp.setActionListener(mb);
}
}
if(immediate != null) {
if(UIComponentTag.isValueReference(immediate)) {
ValueBinding vb = app.createValueBinding(immediate);
catComp.setValueBinding("immediate", vb);
}
else{
boolean boolImmediate =
(new Boolean(immediate)).booleanValue();
catComp.setImmediate(boolImmediate);
}
}
}//setProperties
}//CategoryTag
|
The CategoryTag class extends the
UIComponentTag class to act as a tag class. Notice the
following points from the CategoryTag class implementation above:
- The
getComponentType()method returns the type of the component (category), which tells the JSF framework about the component class used with this tag. Later, we will implement the matching component class.
- The
getRendererType()method returns null, which indicates that there is no renderer class associated with this JSF tag.
- There are two setter methods (
setValue()andsetAction()) in the tag class, which the JSF framework uses to pass the attribute values from the JSP page to theCategoryTagtag handler class.
- The
setProperties()method is used to pass on the values ofvalueandactionattributes from the tag handler class to the matching component class.
Here's how to implement the component class
associated with the xcart:category tag. Look at the
following entry in the faces-config.xml file:
<?xml version="1.0"?>
<faces-config>
.....
<component>
<component-type>category</component-type>
<component-class>xcart.UICategory</component-class>
</component>
<!-- other component instances-->
</faces-config>
|
The component-type
element wraps the type of the component (category). The name of the
component class is specified in the accompanying component-class element
(xcart.UICategory). Now we will implement this UICategory
component class.
The UICategory component
renders the categories and products as buttons. An action event is
generated whenever the user clicks on a category or a product button.
We will extend our UICategory component class from the
UICommand class, which helps us handle action events.
The following code shows the fields and methods of the
UICategory class:
public class UICategory extends UICommand{
private String btnId = null;
private void renderValueAsButton (ResponseWriter writer, String name,
String id, String clientId) {
}
public void encodeBegin(FacesContext context)
throws IOException {
}
private String getActionValueFromRequest(FacesContext fc, String ref,
String tagName) {
}
private Object getModelBeanObject(String ref, FacesContext fc){
}
public void decode (FacesContext context)
throws IOException {
}
public void setBtnId(String id) {
}
public String getBtnId(){
}
}//UICategory
|
The UICategory class
contains only one property (btnId), its setter and getter methods, two
public methods (encodeBegin() and decode()), and three private helper
methods (renderValueAsButton(), getActionValueFromRequest(), and
getModelBeanObject()).
The btnId property contains the ID of the category
or product button that the user clicks. When the
UICategory component
fires an action, the action event handling class calls the
getter method of this property to identify the category or product
the user clicked in the catalog view.
Let's discuss the methods of the UICategory class one
by one.
The renderValueAsButton() method writes the markup to
render a single product or category as a button. The
encodeBegin() method of
the UICategory class calls this method once for every
category and product available in the catalog view.
The renderValueAsButton() method takes the following
four parameters:
-
writer: An object of the
ResponseWriterclass. TherenderValueAsButton()method writes XForms markup on thisResponseWriterobject.
-
name: The name of a product or category.
-
id: The ID of a product or category.
- clientId: uniquely identifies a JSF component on the client side.
The code for the renderValueAsButton() method:
private void renderValueAsButton(ResponseWriter writer, String name,
String id, String clientId){
try{
writer.write("<xforms:submit submission=\"submit\" >");
writer.write("<xforms:label>"+name.trim()+"</xforms:label>");
writer.write("<xforms:action ev:event=\"DOMActivate\">");
writer.write("<xforms:setvalue ref=\"action-performed\">"+
clientId+"@"+id+"</xforms:setvalue>");
writer.write("</xforms:action>");
writer.write("</xforms:submit>");
}
catch (java.io.IOException ie){
ie.printStackTrace();
}
}//renderValueAsButton
|
The above code authors the following markup:
<xforms:submit submission="submit"
xmlns:xforms="http://www.w3.org/2002/xforms"
xmlns:ev="http://www.w3.org/2001/xml-events">
<xforms:label>Computer</xforms:label>
<xforms:action ev:event="DOMActivate">
<xforms:setvalue
ref="action-performed">_id1@1</xforms:setvalue>
</xforms:action>
</xforms:submit>
|
This markup renders a button. The whole markup is hard-coded in the
renderValueAsButton() method, except the
strings "Computer" and "_id1@1" (boldface in the markup above). The string
"Computer" is the value of the second parameter (name). The second
string "_id1@1" is the result of concatenating the third and fourth
parameters (id and clientId). This data uniquely
identifies a product or category.
It consists of two parts separated by an "@" symbol. The first part
(_id1, value of the third parameter) is the ID of the
UICategory component. The second part (1, value of the fourth parameter)
identifies a particular category or product in the catalog view.
Recall from Implementing the xforms-jsf:model component,
where we explained the purpose of inserting an extra
action-performed tag in the application-specific XML. The
renderValueAsButton() method wraps the component and
category (or product) ID in the
action-performed tag. When a user clicks the button
corresponding to a
particular category or product, the data in boldface (_id1@1) is
sent back to the server, where it is used to identify which button was
clicked.
The encodeBegin() method of the UICategory
class is lengthy, so we will implement this method incrementally.
The encodeBegin() method first checks whether the
FacesContext instance is null. If it is
null, it simply throws an exception.
Then it extracts the component ID by calling the
getClientId() method. Next, it gets the
ResponseWriter object so it can
render the markup for this component. These steps are shown in the following
code:
public void encodeBegin(FacesContext context)
throws IOException
{
if (context == null)
throw new NullPointerException();
String clientId = getClientId(context);
ExternalContext externalContext = context.getExternalContext();
ResponseWriter writer = context.getResponseWriter();
.....
}//encodeBegin
|
Now the encodeBegin() method calls the
getModelBeanObject() method. We explained how the
getModelBeanObject() method works in
Implementing the UISelect1 component. The
getModelBeanObject() method returns an object of the
Category class, which contains the data
to be rendered, as shown in boldface below:
public void encodeBegin(FacesContext context)
throws IOException
{
if (context == null)
throw new NullPointerException();
String clientId = getClientId(context);
ExternalContext externalContext = context.getExternalContext();
ResponseWriter writer = context.getResponseWriter();
categoryData = (Category) getModelBeanObject(context,
value);
.....
}//encodeBegin
|
Now we check if the Category instance returned by the
getModelBeanObject() method is null. If it is
null, it means that we have no data (that is, categories
and products) to render in the catalog view.
public void encodeBegin(FacesContext context)
throws IOException
{
if (context == null)
throw new NullPointerException();
String clientId = getClientId(context);
ExternalContext externalContext = context.getExternalContext();
ResponseWriter writer = context.getResponseWriter();
categoryData = (Category) getModelBeanObject(context,
value);
if (categoryData == null){
writer.write ("catalog is empty...");
return;
}
.....
}//encodeBegin
|
Recall Views of the shopping cart, where we discussed that the catalog view contains three boxes: ancestors, subcategories, and products. Now we will code to display these boxes one by one.
The encodeBegin() method calls the
getAncestors() method
of the Category object to check whether the current
category has any ancestors. If the category contains ancestors, the
encodeBegin() method writes markup to display the ancestors in the ancestors
box.
public void encodeBegin(FacesContext context)
throws IOException
{
......
if (categoryData == null){
writer.write ("category data is null...");
return;
}
if (categoryData.getAncestors() != null){
writer.write("<table border=\"1\" width=\"100%\">");
writer.write("<tr>");
writer.write("<td>");
NameValuePair[] ancestors = categoryData.getAncestors();
if (ancestors != null){
for(int x = 0; x < ancestors.length-1; x++){
renderValueAsButton (writer, ancestors[x].getValue(),
ancestors[x].getName(), clientId);
writer.write(" > ");
}
}//if (ancestors != null)
writer.write (ancestors[ancestors.length-1].getValue());
writer.write("</td>");
writer.write("</tr>");
writer.write ("</table>");
}//if (categoryData.getAncestors() != null)
.....
}//encodeBegin
|
Next it calls the getSubCategories() and
getProducts() methods of the Category object to check for the
subcategories and products in the current category. If it contains any subcategory or
product, the encodeBegin() method writes markup to display
subcategories and products as buttons in their respective boxes. For this purpose, it
calls the renderValueAsButton() method explained earlier.
If the category does not contain a subcategory or product,
the encodeBegin() method writes the markup to
display that there is no subcategory or product in the current category.
public void encodeBegin(FacesContext context)
throws IOException
{
......
if (categoryData.getAncestors() != null){
......
}
if (categoryData.getSubCategories() != null){
writer.write("<table border=\"1\" width=\"100%\">");
writer.write("Sub-Categories");
writer.write("<tr>");
writer.write("<td>");
writer.write("<ul>");
NameValuePair[] categories = categoryData.getSubCategories();
for (int x =0; x < categories.length; x++){
writer.write("<li>");
renderValueAsButton (writer, categories[x].getValue(),
categories[x].getName(), clientId);
writer.write("</li>");
}//for
writer.write("</ul>");
writer.write("</td>");
writer.write("</tr>");
writer.write("</table>");
}//if
else{
NameValuePair[] ancestors = categoryData.getAncestors();
writer.write("<table border=\"1\" width=\"100%\">");
writer.write("Sub-Categories");
writer.write("<tr>");
writer.write("</tr>");
writer.write("<tr><td>");
writer.write("<h3> There is no sub-category in the ");
writer.write (ancestors[ancestors.length-1].getValue());
writer.write(" category. . . </h3> ");
writer.write("</td></tr>");
writer.write("<tr>");
writer.write("</tr>");
writer.write("</table>");
}//else
if (categoryData.getProducts() != null){
writer.write("<table border=\"1\" width=\"100%\" >");
writer.write("Products");
writer.write("<tr>");
writer.write("<td>");
writer.write("<ul>");
NameValuePair[] products = categoryData.getProducts();
for (int x =0; x < products.length; x++){
writer.write("<li>");
renderValueAsButton(writer, products[x].getValue(),
products[x].getName(), clientId);
writer.write("</li>");
}//for
writer.write("</ul>");
writer.write("</td>");
writer.write("</tr>");
writer.write("</table>");
}//if
else{
NameValuePair[] ancestors = categoryData.getAncestors();
writer.write("<table border=\"1\" width=\"100%\">");
writer.write("Products");
writer.write("<tr> ");
writer.write("</tr>");
writer.write("<tr><td>");
writer.write("<h3> There is no product in the ");
writer.write (ancestors[ancestors.length-1].getValue());
writer.write(" category. . . </h3> ");
writer.write("</td></tr>");
writer.write("<tr>");
writer.write("</tr>");
writer.write("</table>");
}//else
writer.write("</tr>");
writer.write("</table>");
}//encodeBegin
|
Now let's see the implementation of the
getActionValueFromRequest() method. This method parses the
incoming request and extracts
an identifier from the request (the identifier used to identify which
category or product was clicked by the user). The decode method uses
this method.
The getActionValueFromRequest() method takes the
following parameters:
-
fContext: The
FacesContextinstance.
-
ref: Contains the model bean
(
IncomingXMLInstanceRequest) name and the property that contains the incoming XML request from the client side. See Parsing the incoming XML instance data for details about this model bean.
-
tagName: The name of the tag (
action-performed) that contains the information about the button clicked by the user.
private String getActionValueFromRequest(
FacesContext fContext,
String ref,
String tagName){
Document doc = (Document) getModelBeanObject(fContext, ref);
String value = new String();
if(doc != null) {
NodeList nl = doc.getElementsByTagName(tagName);
if (nl != null) {
for (int i=0; i < nl.getLength(); i++){
if(nl.item(i).getFirstChild().getNodeType() ==
Node.TEXT_NODE ) {
value = nl.item(i).getFirstChild().getNodeValue();
if (value.indexOf('@') != -1) {
int index = value.indexOf('@');
String id = value.substring(0,index);
if (id.equals(getClientId(fContext)))
return value.substring(index+1);
else
return null;
}//if (value.indexOf('@') != -1)
}//if(nl.item(i).getFirstChild()
}//for
}//if(nl != null)
}//if(doc != null)
return null;
}//getActionValueFromRequest()
|
Notice the following points from the
getActionValueFromRequest() method above:
- It first calls the
getModelBeanObject()method, which returns aDOMDocumentobject. TheDOMDocumentobject containsDOMrepresentation of the application-specific XML from the user's request.
- Then it extracts the
action-performedtag from theDOMdocument and extracts its contents. The contents of theaction-performedtag consist of two parts separated by the "@" symbol. The first part identifies the component; the second part identifies the category or the product that was clicked. ThegetActionValueFromRequest()method identifies both the values and returns the product and category ID.
Next, we will discuss the implementation details of the
decode() method.
public void decode (FacesContext context){
if (context == null)
throw new NullPointerException();
String id = getActionValueFromRequest(context,
"#{incomingXMLInstanceRequest.DOMDocument}", "action-performed");
if (id != null){
this.btnId = id;
queueEvent(new ActionEvent(this));
}//if
}//decode
|
You are already familiar with the functionality of the
decode() method. Here, the decode() method is
performing the following steps:
- It checks whether the
FacesContextobject isnull. If it isnull, thedecode()method throws an exception.
- It calls the
getActionValueFromRequest()method explained above.
- The call in step 2 returns a string
value. This value can tell which product or category
was clicked.
- if the ID returned by the
getActionValueFromRequest()method is notnull, thedecode()method will set this ID in thebtnIdproperty of the component (which the event handling logic will use to identify the category or product ID the user clicked).
- Finally, the
decode()method fires an action event. To fire an action event, thedecode()method first instantiates anActionEventobject, passing it the component instance. It then calls thequeueEvent()method, passing it theActionEventobject along with the method call. Note that the event handler class will extract the component object from theActionEventobject, then call thegetBtnId()method of the component to identify the category or product the user clicked.
Implementing the UICategoryActionListener class
The UICategoryActionListener class is associated with the
UICategory component. When the user clicks any category or
product in the catalog view, the UICategory component fires an action event.
this action event is handled by UICategoryActionListener. The
following code shows the methods in the UICategoryActionListener
class:
public class UICategoryActionListener implements
ActionListener {
public void processAction(ActionEvent event) {
}
}//UICategoryActionListener
|
UICategoryActionListener implements the
processAction() method of the
ActionListener interface.
The processAction() method first creates an instance of
the FacesContext object. It then gets an instance of the
model bean from the context by calling the getModelBeanObject() method.
The getModelBeanObject() method returns an object of the
CategoryData class, which contains complete catalog data, as shown in boldface below:
public void processAction(ActionEvent event)
{
FacesContext context = FacesContext.getCurrentInstance();
CategoryData categoryData = (CategoryData) getModelBeanObject
("#{categoryData}", context);
.......
}//processAction
|
Next, the processAction() method calls the
getComponent() method of the event object, which returns the instance of the component
that fires the action event. Then it calls the getBtnId()
method, which returns the ID of the category or product that identifies the category
or product button clicked by the user in the catalog view. The
processAction() method stores the ID in a
String type variable named
actionId:
public void processAction(ActionEvent event){
FacesContext context = FacesContext.getCurrentInstance();
CategoryData categoryData = (CategoryData) getModelBeanObject
("#{categoryData}", context);
UICategory uic = (UICategory) event.getComponent();
String actionId = (String) uic.getBtnId();
.......
}//processAction
|
Next, we check whether the user clicked a category or product. If the user clicked a category, there can be two further subcases: The user can click the root category or any subcategory.
If the user clicked the root category, we will set the
category ID as null (as the root category does not have an
ID). If the user clicked any subcategory in the catalog view, we will set the
category ID as the actionId value. We then call the
setAction() method of the model bean, passing the string "category" to tell the model bean
that the user clicked a category.
public void processAction(ActionEvent event){
.......
UICategory uic = (UICategory) event.getComponent();
String actionId = (String) uic.getBtnId();
if(actionId.equals("null"))
category = true;
else
category = categoryData.isCategory(actionId);
if (category){
if(actionId.equals("null"))
categoryData.getCategoryData().setId(null);
else
categoryData.getCategoryData().setId(actionId);
categoryData.setAction("category");
}//if(category)
.......
}//processAction
|
Now the processAction() method handles the case when
the user clicks a product in the catalog view. Clicking a
product shows the product-specification view. First, we have to prepare
the ProductData model bean, then we have to tell the
CategoryData model bean that the user clicked a product.
So we call the getModelBeanObject() method, which
returns an instance of the ProductData class. The
ProductData model bean was initialized when the first request was made of the
shopping cart, but it does not contains any data. Now we populate the
ProductData object with the data of the product that the
user wants. The product-specification view uses this data to render the
specifications of the product.
Finally, we call the setAction() method of the
CategoryData model bean, passing the string "product" to
tell the CategoryData model bean that the user clicked a product:
public void processAction(ActionEvent event){
.......
if(actionId.equals("null"))
category = true;
else
category = categoryData.isCategory(actionId);
if (category){
.......
}
else {
ProductData productData = (ProductData) getModelBeanObject
("#{ProductData}", context);
productData.setDocument(categoryData.getDocument());
productData.setProductId(actionId);
categoryData.setAction("product");
}//else
}//processAction
|
Implementing the CategoryData model bean
The CategoryData bean is an application-specific model
bean that performs the following actions:
- It parses the XML file, which holds the complete
catalog data comprising all products and categories in our shopping
cart. Note that we are using a simple XML file for back-end data
storage. If you want to use some other type of back-end data storage in your
shopping cart application, you will only need to reimplement the
CategoryDatamodel bean, and the rest of the application will work fine.
- It also instantiates and populates the
Categoryclass required by the components to render the catalog view.
- The
CategoryDatamodel bean also implements action event handling methods to handle the events fired by thexforms-jsf:commandButtoncomponents in the catalog view.
The following code shows the properties and methods of the
CategoryData class:
public class CategoryData{
private String model = null;
private String outcome = null;
private Category categoryData;
private Document document = null;
public CategoryData() {}
public void setModel(String model) {}
public String getModel() {}
public void setDocument (Document doc) {}
public Document getDocument() {}
public void setAction(String action) {}
public Action getAction() {}
public void setCategoryData(Category categoryData) {}
public Category getCategoryData() {}
public boolean isCategory(String id) {}
private Category setCategoryDataValues(String id, Document doc) {}
private void setSubCategoriesAndProducts (Element element,
Category categoryData) {}
private void setNameValuePair(Element[] elements, NameValuePair[]
nameValuePair) {}
private Element[] getElementsByTagName(Element element, String
tagName) {}
private Element getElementById(Element element, String tagName,
String id) {}
private void loadXML(String fileName) {}
public void showCartView(ActionEvent ae) {
}
}//CategoryData
|
The CategoryData
class has a constructor, four properties (document,
model, outcome, and
categoryData), setter and getter methods for these
properties, some private helper methods, and a public showCartView() action event
handling method.
The document property contains the complete catalog of
our shopping cart. The UICategoryActionListener class calls the
getter method (getDocument()) to pass on the document to the
ProductData model bean.
The model property contains the application-specific
XML, which is used to track the category or product the user
clicked in the catalog view. The xforms-jsf:model component
renders the application-specific XML, so the UIModel
component explained in Implementing the xforms-jsf:model component
calls the getter method of the model property to fetch the
application-specific XML.
The outcome property specifies the string used to navigate
to the next page. This property can have any of the following three values:
"category," "product," and "cart." If the value of the outcome
property is "category" (which tells that the user
clicked a category in the catalog view), the next page will be the same:
catalogView.jsp. If the value of the outcome property is
"product" (which tells that the user clicked a product), the next page will be
productView.jsp. If the value of the outcome property is
"cart" (which tells that the user clicked the Show cart button), the next page
will be cartView.jsp.
The categoryData property is an instance of the
Category class, which this model bean instantiates and populates for the
UICategory component.
Let's discuss the methods of the CategoryData class one
by one.
The CategoryData constructor first initializes the
Category object. It then calls the loadXML()
method, passing along the name of the XML file that contains the complete catalog data of our
shopping cart. The constructor also sets the ID property of the newly
created Category object to null. The loadXML() method
parses the XML file and then loads it in the document property of the bean
discussed above.
public CategoryData(){
categoryData = new Category();
categoryData.setId(null);
String path = fc.getApplication().getViewHandler().getResourceURL
(fc, "/shoppingcart.xml");
String url = fc.getExternalContext().encodeResourceURL(path);
this.loadXML(url);
}//CategoryData
|
The CategoryData class implements the
setAction() method. The setAction() method takes a string parameter and sets
it in the outcome property of the model bean. The decode()
method of the UICategory class knows the ID of the category or product that the user clicked in the
catalog view. Before firing the action event, the decode()
method sets this ID in the btnId property of the component.
The event handler class (UICategoryActionListener)
calls the getter method of the btnId to check if the ID belongs
to a category. Then it calls the setAction() method and passes it a
string value "category."
public void setAction(String action){
this.outcome = action;
}//setAction
|
The getAction() method returns a string value, which
the JSF framework uses for navigation. In Using the xcart:category tag to generate the catalog view, we provided the catalogView.jsp
page. Notice the action attribute in the
xcart:category tag shown in
the JSP page. The action attribute specifies the
categoryData.getAction() method name that decides the navigation. The following code shows
the implementation of this method in the model bean:
public String getAction(){
return outcome;
}//getAction
|
The getAction() method returns the value of the outcome property (which
UICategoryActionListener has already set by calling the
setAction() method).
The setCategoryData() method simply takes an instance
of the Category object and assigns it to the
categoryData property.
public void setCategoryData(Category categoryData){
this.categoryData = categoryData;
}//setCategoryData
|
The decode() method extracts
the ID of the category or product the user clicked in the catalog
view and passes it to the event handler class (the
UICategoryActionListener class). When UICategoryActionListener receives the ID,
it instantiates the Category class and sets the
id property of the Category object.
It then calls the setCategoryData() method of the model
bean and passes the Category object along with the method call.
The getCategoryData() method simply calls a helper
method named getCategoryDataValues(). The
getCategoryDataValues() method takes
the same ID that the UICategoryActionListener supplied to
the model bean. The getCategoryDataValues() helper method returns
another Category object that contains the same ID, as well as all the data related to
the same category (list of subcategories, ancestors, and products).
public Category getCategoryData(){
Category catData = getCategoryDataValues(categoryData.getId());
return catData;
}//getCategoryData
|
The CategoryData model bean implements an action event
handling method named showCartView(). Recall the screen
shot for the catalog view from Views of the shopping cart,
which contains a Show cart button. When the user clicks this, the JSF
framework calls the showCartView() method of the model
bean, which simply sets the action property with a "cart" string and causes the
navigation to the cartView.jsp page.
public void showCartView(ActionEvent ae){
this.action = "cart";
}//showCartView
|
In the past four sections, we have seen the implementation of the
catalogView.jsp page, UICategory component, UICategoryActionListener,
and the CategoryData model bean. These four components together form
a set, which renders the catalog view.
In the next two sections, we will implement similar kinds of sets to render the product-specification view and catalog view. But this time, we will not go into low-level details like we did for first set.
Implementing the productView.jsp page, UIProduct component, and ProductData model bean
In the productView.jsp page, we have used three tags from
our XForms-JSF tag library (xforms-jsf:model,
xforms-jsf:selectManyCheckbox, and
xforms-jsf:commandButton) and one
tag (xcart:product) from our shopping cart tag
library.
The JSP author provides the xcart:product tag with a
value attribute, whose value refers to a Product class type
property of a ProductData model bean. For example, look at the following
xcart:product tag in the JSP page:
<?xml version="1.0" encoding="iso-8859-1"?>
<html
xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:xforms="http://www.w3.org/2002/xforms">
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://afictitiousshoppingcart.com/XCart"
prefix="xcart" %>
<%@ taglib uri="http://afictitiousshoppingcart.com/XForms-JSF"
prefix="xforms-jsf" %>
<f:view>
<head>
<xforms-jsf:model value="#{productData.model}"/>
</head>
<body>
<xcart:product value="#{productData.product}"/>
<xforms-jsf:selectManyCheckbox label="Optional Features: "
value="#{productData.selectedOptionalFeatures}" ref="options">
<f:selectItems value="#{productData.optionalFeatures}"/>
</xforms-jsf:selectManyCheckbox>
<xforms-jsf:commandButton label="Add to cart" immediate="false"
action="#{productData.getAction}"
actionListener="#{productData.addProductToCart}">
</xforms-jsf:commandButton>
<xforms-jsf:commandButton label="Back to catalog view"
action="#{productData.getAction}" immediate="true"
actionListener="#{productData.showCatalogView}">
</xforms-jsf:commandButton>
</body>
</f:view>
</html>
|
The xcart:product tag
takes only one attribute: value. You are already familiar
with the functionality of the value attribute. In the above JSP,
the value attribute contains the product property
of the ProductData
model bean. The xcart:product
component implementation calls the getter method of the
productData.product property and gets the
Product instance that contains the
complete details of a single product.
The xcart:product tag renders the details of the
product the user clicked in the catalog view. These details of a product
consist of the name, price, description, and features of a particular
product.
Now let's implement the component class mentioned in the
component-class element of the faces-config.xml file
above: the UIProduct class.
The following code shows the properties and methods of the
UIProduct class:
public class UIProduct extends UIComponentBase{
private String value;
public void encodeBegin(FacesContext context)
throws IOException {
}
public void setValue(Object value) {
}
}//UIProduct
|
The UIProduct class
contains only one property (value), two public methods
(encodeBegin() and setValue()), and four private helper methods
(renderProductSpec(), renderFeatures(), renderNameValue(), and
getModelBeanObject()).
The value property contains the name of the
Product object associated with the UIProduct component. The
UIProduct component uses the value attribute to get the Product
object. It then uses the Product object to fetch the application-specific data
(name, id, price, and description of a product).
Let's look at the encodeBegin() method of the
UIProduct class.
public void encodeBegin(FacesContext context)
throws IOException
{
if (context == null)
throw new NullPointerException();
Product productData = (Product) getModelBeanObject (context,
value);
ResponseWriter writer = context.getResponseWriter();
if (productData == null)
writer.write("<br/>product data is null"); //show nothing
else
{
writer.write("<h3> X-Cart Product Detail </h3>");
writer.write("<table width=\"100%\">");
writer.write("<tr>");
writer.write("<table colspan=\"3\" border=\"1\" width=\"70%\"");
String[] spec = new String[4];
spec[0] = productData.getId();
spec[1] = productData.getName();
spec[2] = productData.getDescription();
spec[3] = productData.getPrice();
String[] labels = {"Product_ID","Product_Name:",
"Product_Description:", "Product_Price:"};
renderProductSpec(writer, spec, labels);
NameValuePair[] features = productData.getFeatures();
if (features != null)
renderFeatures(writer, "Product Features",features);
writer.write("</table>");
writer.write("</tr>");
writer.write("</table>"); //colspan=\"4\" border=\"1\"
}//else
}//encodeBegin
|
The following points explain the above code:
- It first verifies the
FacesContextinstance passed to it. If it isnull, it throws aNullPointerException.
- After verifying the
FacesContextobject, it calls thegetModelBeanObject()method, which returns an instance of theProductobject.
- It fetches the
ResponseWriterobject.
- It then determines whether the
Productinstance isnull. If so, it hard-codes the markup to display that the product isnull.
- If the
Productinstance retrieved in step 2 is notnull, it uses theProductinstance to get the name, description, price, and features of the product.
- Finally, it calls the
renderProductSpec()andrenderFeatures()private helper methods to display the product specifications (product name, description, price, etc.) and product features.
Now we will implement the ProductData model bean.
The ProductData model bean is associated with the
product-specification view. This model bean basically populates the
Product class used by UIProduct and UISelect
components to display the product details like name, price, features, and optional features.
The ProductData model bean also implements the action
event handling methods to handle the action events generated in the
product-specification view and in the edit product view.
The following code shows the implementation of the
ProductData model bean:
public class ProductData{
private Product productData;
private Document document = null;
private String model = null;
private String productId = null;
private boolean isProductSet = false;
private ArrayList optionalFeatures;
private String[] selectedOptionalFeatures = null;
public ProductData() {}
public String getModel() {}
public void setModel(String model){}
public void setDocument(Document doc) {}
public Document getDocument(){}
public String getProductId(){}
public void setProductId(String id) {}
public Product getProductData(){}
public void setProductData(Product pData) {}
public void setOptionalFeatures (String[] optFeatures, Product pd) {}
public Collection getOptionalFeatures (){}
public void setSelectedOptionalFeatures (String[]
selectedOptionalFeatures) {}
public String[] getSelectedOptionalFeatures(){}
public void showCatalogView(ActionEvent ae){
}
public void showCartView(ActionEvent ae){
}
public void addProductToCart(ActionEvent ae){
}
public void saveToCart(ActionEvent ae){
}
}//ProductData
|
The UIProduct class
contains a constructor, six properties (document,
model, productData, productId,
optionalFeatures, and selectedOptionalFeatures),
setter and getter methods for these properties, and four event handler
methods (showCatalogView(), addProductToCart(),
showCartView(), and saveToCart()).
The document property contains the complete catalog
of our shopping cart. UICategoryActionListener calls the
setter method (setDocument()) to pass on the document to
the ProductData model bean.
The model property contains the
application-specific XML. The UIModel component calls the getter method of
the model property to fetch the application-specific XML.
The productId property contains the ID of the product
the user clicked in the catalog view. UICategoryActionListener calls
the setter method (setProductId()) to pass on the product
ID the user clicked.
The optionalFeatures property of bean contains the
optional feature of the product the user clicked in the catalog
view. The UISelect component calls the
getOptionalFeatures() method
to render the optional features of the product as check boxes.
The selectedOptionalFeatures property stores those
optional features the user selects in the product-specification
view. The updateModel() method of the UISelect
component calls the setSelectedOptionalFeatures() method to set the user's
selected optional features that its respective decode() method
retrieved from the request.
The product property of the ProductData
model bean is an instance of the Product class. This property only stores
the record of a single product. The UIProduct and UISelect components
call the getProduct() method to render the product details.
Let's discuss the implementation of the event handling
methods in the ProductData model bean one by one.
The showCatalogView() method takes an instance of the
ActionEvent class along with the method call. Recall the
product-specification view from Views of the shopping cart, where a Back
to catalog view button is shown. When the user clicks this, an
action event occurs. The JSF framework calls the
showCatalogView()
method to handle the event. This method simply sets the
action property
with the "catalogView" string:
public void showCatalogView(ActionEvent ae){
this.action = "catalogView";
}//showCatalogView
|
The showCartView() method takes an instance of the
ActionEvent class along with the method call. Recall the
edit product view from Views of the shopping cart, where a Back
to cart button is shown. When the user clicks this, an action event
occurs. The JSF framework calls the showCartView() method to
handle the event. This method simply sets the action property with the "cart" string.
public void showCartView(ActionEvent ae){
this.action = "cart";
}//showCartView
|
The addProductToCart() event handling method takes an
instance of the ActionEvent object along with the method
call. When the user clicks the Add to cart button in the product-specification
page, the JSF calls the addProductToCart() method to handle the
event. The following code shows the implementation of the
addProductToCart() method:
public void addProductToCart(ActionEvent ae) {
FacesContext context = FacesContext.getCurrentInstance();
Application app = context.getApplication();
ValueBinding vb = app.createValueBinding("#{cartData}");
CartData cdmb = (CartData) vb.getValue(context);
Product pd = getProduct();
String[] selectedFeatures = getSelectedOptionalFeatures();
if (selectedFeatures != null) {
NameValuePair[] optionalFeatures = pd.getOptionalFeatures();
NameValuePair[] new_optionalFeatures = new
NameValuePair[selectedFeatures.length];
for (int x = 0; x < selectedFeatures.length; x++){
for (int y = 0; y < optionalFeatures.length; y++){
NameValuePair pair = optionalFeatures[y];
if (selectedFeatures[x].equals(pair.getName()))
new_optionalFeatures[x] = pair;
}//for
}
pd.setSelectedOptionalFeatures(new_optionalFeatures);
}//if(selectedFeatures != null)
cdmb.addProduct(pd);
this.action = "cart";
}//addProductToCart
|
Note the following points:
- The
addProductToCart()method first gets an instance of theFacesContextobject.
- It fetches an instance of the
CartDatamodel bean from the application context.
- The
addProductToCart()method then calls thegetProduct()method, which returns theProductclass instance that wraps the details of the product the user clicked in the catalog view.
- Next, the
addProductToCart()method calls thegetSelectedOptionalFeatures()method, which returns the list of features the user selected in the product-specification view.
- It checks if the selected optional features are
not
null, then it sets the selected optional features in theselectedOptionalFeaturesproperty of theProductobject.
- After setting the selected features, it adds the
product to the cart. It calls the
addProduct()method of theCartDatamodel bean to add the product to the cart.
- Finally, the
addProductToCart()method sets theactionproperty of the model bean with the "cart" string.
When the user clicks the Save edited product button in
the edit product view, the JSF calls the saveToCart()
method to handle the event. The following code shows the implementation of the
saveToCart() method:
public void saveToCart(ActionEvent ae){
FacesContext context = FacesContext.getCurrentInstance();
Application app = context.getApplication();
ValueBinding vb = app.createValueBinding("#{cartData}");
CartData cd = (CartData) vb.getValue(context);
Product pd = getProduct();
String[] selectedFeatures = getSelectedOptionalFeatures();
if (selectedFeatures == null || selectedFeatures.length == 0)
pd.setSelectedOptionalFeatures(new NameValuePair[0]);
else{
NameValuePair[] optionalFeatures = pd.getOptionalFeatures();
NameValuePair[] new_optionalFeatures =
new NameValuePair[selectedFeatures.length];
for (int x = 0; x < selectedFeatures.length; x++){
for (int y = 0; y < optionalFeatures.length; y++){
NameValuePair pair = optionalFeatures[y];
if (selectedFeatures[x].equals(pair.getName()))
new_optionalFeatures[x] = pair;
}//for(int y = 0; y < optionalFeatures.length; y++)
}//for(int x = 0; x < selectedFeatures.length; x++)
pd.setSelectedOptionalFeatures(new_optionalFeatures);
}//else
Product[] pData = cd.getProducts();
for(int i = 0; i< pData.length; i++){
if( pData[i].getId().equals(pd.getId()) )
cd.setProductAt(pd, i);
}//for
this.action = "cart";
}//saveToCart
|
Note the following code points:
- The
saveToCart()method first gets an instance of theFacesContextobject.
- It then fetches an instance of the
CartDatamodel bean from the application context.
- Then the
saveToCart()method calls thegetProduct()method, which returns theProductclass instance that wraps the details of the product the user clicked in the catalog view.
- The
saveToCart()method calls thegetSelectedOptionalFeatures()method, which returns the list of features the user selected in the product-specification view.
- It then checks if the selected optional features are
nulland sets theselectedOptionalFeaturesproperty of theProductobject asnull.
- If the selected optional features are not
null, it sets the selected optional feature in theselectedOptionalFeaturesproperty of theProductDatamodel bean class.
- After adding the selected features, it calls the
getProducts()method ofCartData, which returns all products in the cart as an array of theProductobject.
- After iterating through all the products until the
current product (edited) is found, it overwrites the edited
product.
- Finally, the
saveToCart()method sets theactionproperty of the model bean with the "cart" string.
Implementing the cartView.jsp page, UICart component, UICartActionListener, and CartData model bean
Look at the following cartView.jsp page, which generates the cart view:
<?xml version="1.0" encoding="iso-8859-1"?>
<html
xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:xforms="http://www.w3.org/2002/xforms">
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://afictitiousshoppingcart.com/XCart"
prefix="xcart" %>
<%@ taglib uri="http://afictitiousshoppingcart.com/XForms-JSF"
prefix="xforms-jsf" %>
<f:view>
<head>
<xforms-jsf:model value="#{cartData.model}"/>
</head>
<body>
<xcart:cart value="#{cartData.products}"
action="#{cartData.getAction}">
<f:actionListener type="model.UICartActionListener"/>
</xcart:cart>
<xforms-jsf:commandButton label="Buy Cart"
action="#{cartData.getAction}"
actionListener="#{cartData.buy}" immediate="true">
</xforms-jsf:commandButton>
<xforms-jsf:commandButton label="Back to catalog view"
action="#{cartData.getAction}"
actionListener="#{cartData.showCatalogView}" immediate="true">
</xforms-jsf:commandButton>
</body>
</f:view>
</html>
|
Note the following points:
- We have included the
xcart:carttag in the body element, which contains two attributes:valueandaction.
- The
xcart:carttag renders the list of all the products a user added to the cart. Thexcart:carttag also renders Remove and Edit buttons with each product in the cart.
- The method of the model bean specified in the
actionattribute (cart.getAction) controls navigation.
- We have associated
an action listener (
UICartActionListener) with thexcart:cartcomponent. When the user clicks the edit or Remove button of any product in the cart view, theUICartActionListenerclass handles the generated action event, which we will discuss shortly.
- We have used two
xforms-jsf:commandButtontags. The first is used to go back to the catalog view; the second is to buy the cart.
Next, we'll implement the component class
associated with the xcart:cart tag.
The following code shows the properties and methods in the
UICart component:
public class UICart extends UICommand{
private String btnId = null;
private String productId = null;
private void renderValueAsButton (ResponseWriter writer, String name,
String id, String clientId) {}
public void encodeBegin(FacesContext context)
throws IOException {}
public void decode (FacesContext context) {}
private String getActionValueFromRequest(String retValue,
FacesContext fc, String ref, String tagName) {}
}//UICart
|
The UICart class
contains two properties (btnId, productId),
setter and getter methods for the
properties, two public methods (encodeBegin() and
decode()), and three
private helper methods (renderValueAsButton(),
getModelBeanObject(), and getActionValueFromRequest()).
The btnId property contains the ID of the Edit or
Remove button the user clicks in the cart view. When the
UICart component fires an action, the action event handling class calls the
getter method of this property to compare the button the user
clicked against a particular product in the cart view.
The productId property contains the ID of the product
whose Edit or Remove button the user clicks in the cart view. When
the UICart component fires an action, the action event
handling class calls the getter method of the productId property to
get the product ID.
Let's discuss the methods of the UICart class one by
one.
The renderValueAsButton() method writes the markup to
render a single product as a button. The encodeBegin()
method of the UICategory class calls this method once for every product
available in the cart view.
The renderValueAsButton() method takes the following
four parameters:
-
writer: An object of the
ResponseWriterclass. Itswrite()method is used to write the response markup.
-
name: The name of a product.
-
id: The ID of a product.
- clientId: Uniquely identifies a component on the client side.
The code for the renderValueAsButton() method:
private void renderValueAsButton(ResponseWriter writer, String name,
String id, String clientId){
try {
writer.write("<xforms:submit submission=\"submit\"");
writer.write("
xmlns:xforms=\"http://www.w3.org/2002/xforms\">");
writer.write("<xforms:label>"+name.trim()+"</xforms:label>");
writer.write("<xforms:action ev:event=\"DOMActivate\"");
writer.write("
xmlns:ev=\"http://www.w3.org/2001/xml-events\">");
writer.write("<xforms:setvalue ref=\"action-performed\">"+
clientId+"@"+id+"@"+name.trim()+"</xforms:setvalue>");
writer.write("</xforms:action>");
writer.write("</xforms:submit>");
}
catch (java.io.IOException ie){
ie.printStackTrace();
}
}//renderValueAsButton
|
The above code authors the following markup:
<xforms:submit submission="submit"
xmlns:xforms="http://www.w3.org/2002/xforms"
xmlns:ev="http://www.w3.org/2001/xml-events">
<xforms:label>Intel_Pentinum4</xforms:label>
<xforms:action ev:event="DOMActivate">
<xforms:setvalue ref="action-performed">
_id1@1@edit
</xforms:setvalue>
</xforms:action>
</xforms:submit>
|
This markup renders a button. The whole markup is hard-coded
in the renderValueAsButton() method, except the
strings "Intel_Pentinum4" and "_id1@1@edit" (boldface in the markup
above). The first string "Intel_Pentinum4" is the value of the second parameter
(name). The second string "_id1@1@edit" uniquely
identifies the product whose Remove and Edit button the user clicks. The second string
consists of three parts, each of which is separated by an "@" symbol. The
first part (_id1) is the ID of the component. The second part (1) identifies
a particular product in the catalog view. The third part
(edit) identifies the button clicked.
Recall Implementing the xforms-jsf:model component, where we explained the purpose of inserting an extra
action-performed tag in the application-specific XML. The
renderValueAsButton() method wraps the component, product
ID, and button name (edit or remove) in the action-performed tag. When a user clicks
the button corresponding to a particular product in the cart view, the string
"_id1@1@edit" is sent back to the server.
The encodeBegin() method of the UICart
class is lengthy, so we will implement this method incrementally.
The encodeBegin() method first checks whether the
FacesContext instance is null. If it is
null, it simply throws an exception.
Then it extracts the component ID by calling the
getClientId() method.
Next, it gets the ResponseWriter object so that it can
render the markup for this component. See these steps in the following code:
public void encodeBegin(FacesContext context) throws IOException {
if (context == null)
throw new NullPointerException();
String clientId = getClientId(context);
ExternalContext externalContext = context.getExternalContext();
ResponseWriter writer = context.getResponseWriter();
double totalPrice = 0;
.....
}//encodeBegin
|
Now the encodeBegin() method calls the
getModelBeanObject() method. We explained how this
method works in Implementing the UISelect1 component. The
getModelBeanObject() method returns an array-type object
Product class, which contains
the data to be rendered. See the boldface code below:
public void encodeBegin(FacesContext context)
throws IOException {
if (context == null)
throw new NullPointerException();
String clientId = getClientId(context);
ExternalContext externalContext = context.getExternalContext();
ResponseWriter writer = context.getResponseWriter();
double totalPrice = 0;
Product[] products = (Product[]) getModelBeanObject(context,
value);
.....
}//encodeBegin
|
Now we check if the Product instance returned by the
getModelBeanObject() method is null. If it is
null, it means that we have no
data (products) to render in the cart view.
public void encodeBegin(FacesContext context)
throws IOException {
......
Product[] products = (Product[]) getModelBeanObject(context,
value);
if (products == null){
writer.write ("Cart is empty......");
return;
}//if(products == null)
.....
}//encodeBegin
|
Now the encodeBegin() method writes the markup to
display each product and its price in tabular format from the
Product object, as shown in boldface below:
public void encodeBegin(FacesContext context)
throws IOException {
......
if (products == null){
writer.write ("<br><h3> Cart is
empty......</h3>");
return;
}//if (products == null)
writer.write("\r\n X-Cart View <br/> ");
writer.write("<table border=\"1\">");
writer.write("<ol type=\"1\">");
for (int x = 0; x<products.length; x++){
Product product = products[x];
double price = Integer.parseInt(product.getPrice());
NameValuePair[] pair = product.getSelectedOptionalFeatures();
if(pair != null ){
for(int y = 0; y < pair.length; y++){
price += Integer.parseInt(pair[y].getValue());
}
}//if(pair != null )
totalPrice += price;
writer.write("<tr>");
writer.write("<td>");
writer.write("<li>"+product.getName()+"</li>");
writer.write("</td>");
writer.write("<td>");
renderValueAsButton(writer, " Edit ",
new Integer(x).toString(), clientId);
writer.write("</td>");
writer.write("<td>");
renderValueAsButton(writer, " Remove ",
new Integer(x).toString(), clientId);
writer.write("</td>");
writer.write("<td>");
writer.write("" + price);
writer.write("</td>");
writer.write("</tr>");
}//for (int x = 0; x<products.length; x++)
writer.write("</ol>");
writer.write("</table>");
writer.write("<table border=\"1\">");
writer.write("<ol>");
writer.write("<tr>");
writer.write("<td>");
writer.write("<li> Total Cart Price </li>");
writer.write("</td>");
writer.write("<td>");
writer.write(""+totalPrice);
writer.write("</td>");
writer.write("</tr>");
writer.write("</ol>");
writer.write("</table>");
}//encodeBegin
|
Now we'll implement the
getActionValueFromRequest() method. The
decode() method calls this method to get the
ID of the product whose Edit or Remove button the user clicked in
the cart view.
The getActionValueFromRequest() method takes the
following four parameters:
-
retValue: Contains the string value "id" or
"action," which decides the return value of the method.
-
fc: The
FacesContextinstance.
-
ref: Contains the model bean (that is,
IncomingXMLInstanceRequest) name and the property that contains the incoming XML request from the client side. See Parsing the incoming XML instance data for details about this model bean.
-
tagName: The name of the tag (that is,
action-performed) that contains the clicked button information.
private String getActionValueFromRequest(String retValue,
FacesContext fc, String ref, String tagName){
String value = new String();
Document doc = (Document) getModelBeanObject(fc, ref);
if(doc != null){
NodeList nl = doc.getElementsByTagName(tagName);
if (nl != null){
for (int i=0; i < nl.getLength(); i++){
if (nl.item(i).getFirstChild().getNodeType() ==
Node.TEXT_NODE ){
value = nl.item(i).getFirstChild().getNodeValue();
if (retValue.equals("id")){
if (value.indexOf('@') != -1){
int index = value.indexOf('@');
String id = value.substring(0,index);
if (id.equals(getClientId(fc)))
return value.substring(index+1,
value.lastIndexOf('@'));
else
return null;
}//if(value.indexOf('@') != -1)
}//if(retValue.equals("id"))
else
return value.substring(value.lastIndexOf('@')+1);
}//if
}//for
}//if(nl != null)
}//if(doc != null)
return null;
}//getActionValueFromRequest
|
Note the following points from the
getActionValueFromRequest() method:
- The method first gets the
property from the model bean that contains the client request as
DOM Documentobject.
- Then it extracts
the
action-performedtag from theDOMdocument and extracts its value.
- Finally, it checks the value of the
retValueparameter. If it is "id," then it returns the product ID whose Edit or Remove button is clicked. Otherwise, it returns the ID of the button -- edit or Remove -- from the request.
Next, we will discuss the implementation details of the
decode() method.
public void decode(FacesContext context){
if (context == null)
throw new NullPointerException();
String id = getActionValueFromRequest("id", context,
"#{incomingXMLInstanceRequest.DOMDocument}", "action-performed");
String action = getActionValueFromRequest("action", context,
"#{incomingXMLInstanceRequest.DOMDocument}","action-performed");
if(id != null){
this.btnId = action;
this.productId = id;
queueEvent(new ActionEvent(this));
}//if(id != null)
}//decode
|
Note the following points in the decode() method
implementation:
- It first verifies the
FacesContextinstance passed to it by the JSF framework. If it isnull, it throws aNullPointerException.
- If the
FacesContextinstance is notnull, thedecode()method calls thegetActionValueFromRequest()method, passing it the string "id" in theretValueparameter, as explained above. This method returns the ID of the product that the user clicked.
- The
decode()method again calls thegetActionValueFromRequest()method, passing it the string "action." It returns the name of the button the user clicked (edit or Remove).
- If the product ID returned by the
getActionValueFromRequest()method is notnull, it sets the product name ID in thebtnIdproperty and the product ID in theproductIdproperty of the component (that the event handling logic uses to identify the Edit or Remove button of a particular product the user clicked).
- Finally, the
decode()method fires an action event by instantiating anActionEventobject, passing it the component instance. It then calls thequeueEvent()method, passing it theActionEventobject along with the method call. Note that the event handler class extracts the component object from theActionEventobject and calls thegetBtnId()method of the component to identify the product the user clicked.
Next, we will implement the UICartActionListener.
The UICartActionListener class is associated with the
UICart component. When the user clicks the Edit or
Remove button of a particular product in the cart view, the UICart component
fires an action event. This action event is handled by the
UICartActionListener class. The implementation of the UICartActionListener
class:
public class UICartActionListener implements ActionListener {
public void processAction(ActionEvent event) {
}
private Object getModelBeanObject(FacesContext context,String value){
}
}//UICartActionListener
|
The UICartActionListener class contains two methods
(processAction() and getModelBeanObject()). We explained the
getModelBeanObject() method in
Implementing the UISelect1 component. The
processAction() method processes the action event fired by the component:
public void processAction(ActionEvent event) {
FacesContext context = FacesContext.getCurrentInstance();
CartData cd = (CartData) getModelBeanObject("#{cartData}",
context);
ProductData pdm = (ProductData)
getModelBeanObject("#{editProductData}", context);
UICart uic = (UICart) event.getComponent();
String productId = uic.getProductId();
String btn = uic.getBtnId();
if(btn.equals("Edit")){
Product pd = cd.getProductAt(productId);
if (pd != null)
pdm.setProduct(pd);
cd.setAction("edit");
}//if(btn.equals("Edit"))
else{
cd.removeProduct(productId);
cd.setAction("remove");
}//else
}//processAction
|
Note the following points:
- The
processAction()method first retrieves theFacesContextinstance.
- It then fetches the
CartDataandProductDatamodel bean instances from the application context.
- Next, it calls the
getComponent()method of the event object, which returns an object of the component class that fired the event.
- Then it calls the
getBtnId()andgetProductId()methods of the component class, which returns the ID of the button the user clicked and the product ID against that button, respectively.
- It then checks if the button ID is Edit. if it is, it fetches the product from the cart against
the product ID and sets it in the
ProductDatamodel bean by calling itssetProduct()method.
- If the button ID is not Edit, it simply calls the
removeProduct()method of theCartDatamodel bean, passing it the product ID. This method call simply removes the product from the cart.
Now let's implement the CartData model bean.
The CartData model bean stores all the products added
by the user in the cart. The UICart component uses this
CartData model bean to fetch the list of products added to the cart and also to handle
some action events that occurred in the cart view.
The following code shows the implementation of the CartData
class:
public class CartData{
protected String model = null;
protected String outcome = null;
protected Product[] products = null;
public String getAction(){
}
public void setAction (String outcome){
}
public String getModel(){
}
public void setModel(String model){
}
public Product [] getProducts(){
}
public Product getProductAt(String index){
}
public void setProductAt(Product pd, int index)
}
public void removeProduct(String index){
}
public void addProduct(ProductData productData){
}
public void showCatalogView(ActionEvent ae){
}
public void buy(ActionEvent ae){
}
}//CartData
|
The CartData class
has three properties (model, outcome, and
products), setter and getter
methods for these properties, some private helper methods, four public
methods (getProductAt(), setProductAt(),
addProduct(), and removeProduct()),
and two action event handling methods (showCatalogView()
and buy()).
The model property contains the application-specific
XML, which is used to track the product the user clicked in the cart
view. The xforms-jsf:model component renders
the application-specific XML, so the UIModel component
explained in Implementing the xforms-jsf:model component
calls the getter method of the model property to fetch the
application-specific XML.
The outcome property specifies the string used for navigation.
This property can have one of three values:
"catalogView," "buy," or "edit." If the value of the
outcome property is "catalogView" (which indicates that the user clicked the Back to catalog
view button in the cart view), the next page will be the
catalogView.jsp. If the value is
"edit" (which indicates that the user clicked the Edit button of particular product),
the next page will be editProductView.jsp. If the value is "buy" (which indicates that the user
clicked the Buy button), the next page will be checkout.jsp.
The products property is an array of the Product
object, which stores all the products added by the user to the cart. The
UICart component calls its getter method to fetch the list of products added
to the cart.
Let's discuss the methods of the CartData class one by
one.
The CartData class implements the
setAction() method. The
setAction() method takes a string parameter and sets it in
the outcome
property of the model bean. The action event handling logic calls the
setAction() method, passing it a string value.
public void setAction(String action){
this.outcome = action;
}//setAction
|
The getAction() method returns a string value, which
the JSF framework uses for the navigation. In the cartView.jsp page
above, notice the action attribute in the
xcart:cart tag. The action
attribute specifies the cartData.getAction method name that decides
the navigation. The following code shows the implementation of this method
in the model bean:
public String getAction(){
return outcome;
}//getAction
|
The getAction()
method returns the value of the outcome property (which
the event handling logic has already set by calling the setAction()
method).
The getProductAt() method returns a product from the
array of Product objects at some specific index. It takes the
product index along with the method call. The UICartActionListener calls
this method to fetch the product at some specific location in the cart.
public Product getProductAt(String index) {
if ( productsVector.size() > 0 )
return (Product)productsVector.elementAt(Integer.parseInt(index));
return null;
}//getProductAt
|
The setProductAt() method sets a product at some
specific index in the array of the Product object. It takes the
Product object and index along with the method call. The UICartActionListener
calls this method to set the product at a particular location:
public void setProductAt(Product pd, int index) {
if ( pd != null )
productsVector.setElementAt(pd, index);
}//setProductAt
|
The addProduct() method adds the product to the cart
that is passed to it along with the method call. The event handling logic
behind the Add to cart button in the product-specification view calls
the addProduct() method, passing it the Product
object along with the method call.
public void addProduct(Product productData) {
if (productData != null) {
productsVector.addElement(productData);
}
}//addProduct
|
The removeProduct() method removes the product from the
array of Product objects at the given index. When the user
clicks the Remove button in the cart view, the UICartActionListener
calls the removeProduct() method, passing it the index of the
product in the cart.
public void removeProduct(String index) {
productsVector.removeElementAt(Integer.parseInt(index));
}//removeProduct
|
The CartData model bean implements an action event
handling method named showCatalogView(). Recall the
screenshot for the cart view from Views of the shopping cart, which
contains a Back to catalog view button. When the user clicks this, the
JSF framework calls the showCatalogView() method of the model
bean, which sets the action property with a "catalogView"
string and causes the navigation to the catalogView.jsp page.
public void showCatalogView(ActionEvent ae){
this.outcome = "catalogView";
}//showCatalogView
|
The screenshot for the cart view shown in Views of the shopping cart contains a Buy button. When the user clicks
this, the JSF framework calls the buy() method of
the model bean, which sets the action property with a "buy"
string and causes the navigation to the checkout.jsp page.
public void buy(ActionEvent ae){
if(getProducts().length == 0){
return;
}
this.outcome = "buy";
}//buy
|
We have placed the source code for our complete XForms-JSF shopping cart application in the section9.zip file available in the source code download of this tutorial; see Resources. When you unzip the section9.zip file, you will find that it contains an xcart.jar file (that is, the shopping cart-specific tag library developed in this section), ShoppingCart.war file (that is, the XForms-JSF Shopping cart application developed in this section), and a folder named ShoppingCart. The ShoppingCart folder contains the complete source code for our XForms-JSF shopping cart application, including all the JSP pages, model beans, and action listeners developed for our sample shopping cart application.
To try our XForms-JSF shopping cart application, you deploy the ShoppingCart.war file in your application server. Be sure to use the following URL in the address bar of your XForms browser:
http://localhost:8080/ShoppingCart
|
Once the catalog view displays, you can browse through our shopping cart, as we discussed in Views of the shopping cart.

