Customizing WebSphere Commerce by using the Struts framework

Lessons learned

This article describes a framework that extends WebSphere® Commerce using Struts, and gives you some tips on implementation. Sample code shows you how to use the framework to implement a simple Web page.

Share:

Esther Yu (esthery@ca.ibm.com), Software Developer, IBM

Photo: Esther YuEsther Yu is a Business Solution Architect with the WebSphere Commerce Solution Enablement team at the IBM Toronto Lab, Canada. She has over eight years of experience developing WebSphere Commerce solutions for ibm.com and other customers.



Chris Felix (chrisfel@ca.ibm.com), Software Engineer, IBM

Photo: Chris FelixChris Felix is a Software Developer and Agile ScrumMaster in the IBM Software Services for WebSphere organization at the IBM Toronto Lab, Canada. He is experienced in developing Web 2.0 applications that leverage Web services provided through WebSphere Process Server. He was recently engaged in a project that required a mobile front end, allowing him to gain experience in iPhone development.



05 November 2008

Most WebSphere® Commerce customers extend or customize the product to fit their specific needs. WebSphere Commerce is based on the Struts framework. To take full advantage of what Struts has to offer, you need to understand what to watch out for during customization. This article describes the framework we developed to extend WebSphere Commerce by using Struts. The sample code illustrates:

  • How we use Struts Action, ActionForms, and WebSphere Commerce commands.
  • How to avoid common pitfalls.
  • The workaround we took to address some limitations of the base code.

Today, many J2EE™ developers are using the Struts framework to develop Web applications. Struts has been one of most popular framework for the MVC architecture. WebSphere Commerce has a unique command framework, which was developed long before Struts emerged. Although since version 6, WebSphere Commerce has adopted Struts, not many developers know how to extend it using Struts. The goal of this article is to provide references and options to those developers who might run into the same problems as we did. The target readers of this article are experienced J2EE developers who want to customize WebSphere Commerce using Struts. Readers also need basic understanding of the WebSphere Commerce command framework.

Introduction

Struts is one of the most popular MVC framework. Struts' strength lies in using the "Action" class as the controller. The Action class is in charge of the request processing, interacting with model layer to create beans or objects used by JSPs, and deciding which JSP page to forward the request, depending on the user's actions. JSP, as the "view" layer, is responsible for retrieving any objects or beans that may have been previously created by the Action class, and extracting the dynamic content from the beans for insertion within static templates.

WebSphere Commerce allows user to extend the BaseAction class by customizing preprocess, postprocess, getRequestParameters, and invokeService methods. In the framework described in this article, Action classes that extend BaseAction are created to handle presentation layer logic and act as controllers to manage the page flows. WebSphere Commerce's commands are used to perform business logic. Struts ActionForm and tag libraries are used in JSPs to create the view. Let's use a simple express order form page to describe this framework in detail.


Overview of the extended WebSphere Commerce Struts framework

Figure 1 shows the express order form page. The page allows users to enter multiple part numbers and quantities and add them to the shopping cart or wish list at once. Users can also use the drop down menu to add more entry elements in the form.

Figure 1. Express order form page
Figure 1. Express order form page

Figure 2 shows how the extended WebSphere Commerce Struts framework works. Each dynamic page is associated with two action classes, one for displaying the page and the other for submitting the page. For example, expressOrderForm.jsp is linked to ExpressOrderFormDisplay action and ExpressOrderFormSubmit action. The display action populates the XICExpressOrderForm to make all the data ready for display, and then forwards to the JSP page where standard Struts tags are used to access the data from the ActionForm class. When a form is submitted from that page, the submit action, ExpressOrderFormSubmit, is invoked that takes the information from the ActionForm and calls the appropriate commands to handle the business logic and to update the database if necessary. The submit action then forwards or redirects to different actions or pages, depending on what submit process is triggered or whether the process is successful.

Figure 2. Web diagram of the express order form flow
Web diagram of the express order form flow

Importing and running the code in WebSphere Commerce Developer Edition

You can download the sample zip file containing the code, ExpressOrderForm.zip:

  1. Import the Java™ source code under the WebSphereCommerceServerExtensionsLogic project into the workspace.
  2. Import the Web content artifacts into the Web project. This includes expressOrderForm.jsp, expressOrderForm.js , and expressOrderForm_en_US.properties.
  3. Update web.xml to include struts-config-experssOrderForm.xml. Find the line containing the existing Struts configuration files and add struts-config-experssOrderForm.xml at the end:
    <param-value>/WEB-INF/struts-config.xml,/WEB-INF/struts-config-GiftCenter.xml,
    /WEB-INF/struts-config-migrate.xml,/WEB-INF/struts-config-ext.xml,
    /WEB-INF/struts-config-expressOrderForm.xml</param-value>
  4. Update ACPolicy. Extract ExpressOrderFormPolicy.xml from the zip file into WCDE_installdir\xml\policies\xml directory. Then run the following command:
    acpload <db_name> <db_user> <db_password> ExpressOrderFormPolicy.xml 
    <schema_name>
  5. Update storeErrorMessage.propreties by adding the following lines. This includes form validation messages and an error message when an exception happens during the "Add to wish list" operation.
    ERR_CREATE_OR_UPDATE_IITEM = Some of the SKUs entered are invalid. Please revise 
    your entries. 
    BlankFormError=The form is blank, please enter quantity and part number. 
    QuantityError = Quantity should be an integer less than {1}. Please revise your 
    entry on line {0}. 
    PartNumberError = SKU number should not be blank. 
    Please enter a part number on line {0}.
  6. Start the server and load the following URL in a Web browser:
    http://localhost/webapp/wcs/stores/servlet/ExpressOrderFormDisplay?langId=-1
    &storeId=10001&catalogId=10001

Extending WebSphere Commerce BaseAction

The WebSphere Commerce Information Center provides an overview of the WebSphere Commerce Struts framework.

In the WebSphere Commerce base code, for views, the BaseAction class forwards directly to the JSPs and the JSPs use various data beans to retrieve data to be displayed on the Web pages. ActionForms are not used in the starter store JSPs. When a form is submitted, BaseAction populates requestProperties with the parameters from the request and then invokes the controller commands to execute specific business logic. Following one of Struts best practices, for each dynamic page we created two Action classes, one for displaying the page and one for submitting the page. The extension we built starts from BaseAction. Figure 3 shows that the BaseAction is extended.

Figure 3. Class diagram of XICBaseDisplayAction and XICBaseSubmitAction
Class diagram of XICBaseDisplayAction and XICBaseSubmitAction

Details on display actions

XICBaseDisplayAction

As shown in the above class diagram, XICBaseDisplayAction (Listing 1) extends the WebSphere Commerece BaseAction. In the preProcess() method, it first invokes super.preProcess(), and then calls the prepareView() method. prepareView() is an abstract method here and the concrete implementation is provided by the display action classes of each page to prepopulate ActionForms specific for each page. This Action class also overrides the BaseAction isValidationEnabled() method to return false. This is because the display actions usually do not need to validate the form input fields.

Listing 1. XICBaseDisplayAction code
public abstract class XICBaseDisplayAction extends BaseAction {
  private static final String CLASSNAME = XICBaseDisplayAction.class
  .getName();

  protected TypedProperty preProcess(RequestHandle handle,
   ActionMapping mapping, ActionForm form, Map requestParameters,
 HttpServletRequest request, HttpServletResponse response,
      String viewName) throws RequestException {

    final String METHODNAME = "preProcess";
    boolean traceEnabled = ECTrace
      .traceEnabled(ECTraceIdentifiers.COMPONENT_SERVER);
    if (traceEnabled) {
      ECTrace.entry(ECTraceIdentifiers.COMPONENT_SERVER, CLASSNAME,METHODNAME);
    }

    TypedProperty prop = super.preProcess(handle, mapping, form, requestParameters, 
    request, response, viewName);
    prepareView(handle, form, request, response);
    if (traceEnabled) {
      ECTrace.exit(ECTraceIdentifiers.COMPONENT_SERVER, CLASSNAME,METHODNAME, prop);
    }
    return prop;
  }

  protected boolean isValidationEnabled() {
   return false;
  }

  protected abstract void prepareView(RequestHandle handle,
   ActionForm form, HttpServletRequest request,
   HttpServletResponse response) throws RequestException;

......
}

XICExpressOrderFormDisplayAction

In XICExpressOrderFormDisplayAction (Listing 2), the prepareView() method populates the drop down menu to add more lines.

Listing 2. XICExpressOrderFormDisplayAction code
public class XICExpressOrderFormDisplayAction extends
  XICBaseDisplayAction {
 protected void prepareView(RequestHandle handle, ActionForm form,
  HttpServletRequest request, HttpServletResponse response)
     throws RequestException {
    final String METHODNAME = "handle";
    ECTrace.entry(ECTraceIdentifiers.COMPONENT_EXTERN, this
     .getClass().getName(), METHODNAME);
    //Get handle to express order form
    XICExpressOrderForm orderForm = (XICExpressOrderForm) form;

    int defaultNumItems;
    try {
      defaultNumItems = 10; 
    } catch (Exception e) {
      ECTrace
       .trace(
        ECTraceIdentifiers.COMPONENT_EXTERN,
        this.getClass().getName(),
        METHODNAME,
        "Could not resolve ExpressOrderForm.QuickShip." +
        "DefaultNumItems...setting default number of " +
        "items to 1");
      defaultNumItems = 1;
    }//end try
    orderForm.addNBlankLines(defaultNumItems
      - orderForm.getOrderItems().size());

    //Populate drop downs
    populateDropDowns(orderForm);

    ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, this.getClass()
        .getName(), METHODNAME);
  }//end handle

  public void populateDropDowns(XICExpressOrderForm orderForm) {
    final String MAX_NUM_OF_LINES = "30";
    final String ONE_LINE = "1";
    final String FIVE_LINE = "5";
    final String TEN_LINE = "10";
    orderForm.getLinesToAddList().clear();
    orderForm.setMaxItemQuantity(MAX_NUM_OF_LINES);
    if ((1 + orderForm.getOrderItems().size()) <= Integer
      .parseInt(MAX_NUM_OF_LINES)) {
     orderForm.getLinesToAddList().add(
       new XICDropDownListItem(ONE_LINE, ONE_LINE));
    }
    if ((5 + orderForm.getOrderItems().size()) <= Integer
      .parseInt(MAX_NUM_OF_LINES)) {
     orderForm.getLinesToAddList().add(
      new XICDropDownListItem(FIVE_LINE, FIVE_LINE));
    }
    if ((10 + orderForm.getOrderItems().size()) <= Integer
     .parseInt(MAX_NUM_OF_LINES)) {
     orderForm.getLinesToAddList().add(
      new XICDropDownListItem(TEN_LINE, TEN_LINE));
    }
  }
}

Defining XICExpressOrderFormDisplayAction in the Struts config file

In struts-config-experssOrderForm.xml, the view is defined in Listing 3.

Listing 3. Define a view in the Struts config file
<global-forwards>
<forward className="com.ibm.commerce.struts.ECActionForward" 
	name="ExpressOrderFormDisplay/10001" 
	path="/ShoppingArea/expressOrderForm.jsp"/>
</global-forwards>
<action-mappings type="com.ibm.commerce.struts.ECActionMapping">
<action path="/ExpressOrderFormDisplay" type="com.ibm.commerce.struts.xic.webstore.
 actions.XICExpressOrderFormDisplayAction"name="XICExpressOrderForm" 
 scope="request" validate="false">
  <set-property property="https" value="10001:1"/>
  <set-property property="credentialsAccepted" value="0:P"/>
</action>

Notice that in the <action> tag, the type attribute of the action is the actual display action class XICExpressOrderFormDisplayAction, and the name attribute is the ActionForm class, XICExpressOrderForm.

Details on submit actions

XICBaseSubmitAction

In one of our recent projects, most of the Web pages have multiple submit buttons and links in one single form and they all submit to one action. We wrote the XICBaseSubmitAction (Listing 4) to handle different submit actions more easily. You will notice a few things in this Action class:

  • It overrides invokeServices() in the BaseAction class where it uses reflection, for example, calling getMethod() and invokeMethod() to invoke the corresponding method based on the submit action. submitAction is always a hidden parameter in the form on the JSP pages and you can retrieve it via XICBaseActionForm.getSubmitAction().
  • Method clearRequestProperties() is called to remove the parameters from requestProperties. This is often needed to void building up long URL query strings that are unnecessary. The subclasses of XICBaseSubmitAction can optionally set clearReqProp to true. Parameters like langId, storeId, and catalogId are not stripped because those are usually needed.
  • Method clearRequestPropertiesForAll() indicates whether to clear the request properties after any and all action handler methods are executed. By default, this returns false. getClearRequestProperties() indicates whether to clear request properties after one submit action handler method completes. This method allows finer grained control over which action handlers will clear request properties.
  • Method unknownAction() can be overridden in the subclasses to handle form submit actions that do not map to any obvious icon or text.
  • isValidationEnabled() returns true because the submit action usually needs to validate the user input. The subclasses can override the return value based on the requirements of each page.
Listing 4. XICBaseSubmitAction code
public class XICBaseSubmitAction extends BaseAction {
 private boolean clearReqProp = false;

 private static final String CLASSNAME = XICBaseSubmitAction.class
  .getName();

 protected Map invokeService(ActionMapping mapping, ActionForm form,
    Map inMap, HttpServletRequest request,
    HttpServletResponse response) throws RequestException {
   final String METHODNAME = "process";
   RequestHandle handle = (RequestHandle) request
    .getAttribute(ECAttributes.ATTR_EC_REQUEST_HANDLE);
   Map respProp = null;
   try {
      ViewCommandContext vContext = handle.getViewCommandContext();
      XICBaseActionForm actionForm = (XICBaseActionForm) form;
      if (form == null) {
       //go to generic error view
       ECTrace.trace(ECTraceIdentifiers.COMPONENT_EXTERN, this
        .getClass().getName(), METHODNAME,
         "XICBaseActionForm was null");
       ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, this
         .getClass().getName(), METHODNAME);
       throw new ECApplicationException(ECMessage._ERR_GENERIC, this
         .getClass().getName(), METHODNAME);
      }//end if
      //ASSERT form is not null
      //Get the action
      String action = this.getAction(actionForm, vContext);

      //Form passed simple validation, so call action
      //Invoke action handler method
      Method method = this.getMethod(getSubmitActionHandlerInst(),       action);
      if (method == null) {
        //No method found to invoke to handle action
        ECTrace.trace(ECTraceIdentifiers.COMPONENT_EXTERN, this
         .getClass().getName(), METHODNAME,
         "No method found to handle action: " + action);
        ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, this
         .getClass().getName(), METHODNAME);
        throw new ECApplicationException(ECMessage._ERR_GENERIC, this
         .getClass().getName(), METHODNAME);
      }//end if
      //ASSERT:We have valid method to invoke
      respProp = this.invokeMethod(method, actionForm, action,        vContext, handle);
      if (respProp == null) {
       //Method invocation failed
       ECTrace.trace(ECTraceIdentifiers.COMPONENT_EXTERN, this
        .getClass().getName(), METHODNAME,
        "Method invocation failed for action: " + action);
       ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, this
        .getClass().getName(), METHODNAME);
       throw new ECApplicationException(ECMessage._ERR_GENERIC, this
        .getClass().getName(), METHODNAME);
      }//end if

      //Clear form submit action
      actionForm.setSubmitAction("");
      //In case developer is not clearing request properties 
      // (i.e using command chaining)
      //we need to get rid of submit action from requestProperties
      vContext.getRequestProperties().remove(action + ".x");
      vContext.getRequestProperties().remove(action + ".y");
      vContext.getRequestProperties().remove(action);
      //Clear request properties (IE fix for long query string)
      if (this.clearRequestPropertiesForAll()
          || this.getClearRequestProperties()) {
        this.clearRequestProperties(vContext.getRequestProperties());
      }//end if
      respProp.putAll(vContext.getRequestProperties());
    } catch (ECException e) {
      if (e.isRecoverable()) {
       ECTrace.exit(ECTraceIdentifiers.COMPONENT_SERVER, CLASSNAME,
        METHODNAME, "exception, retry request ");
       throw new RetryRequestException(e);
      } else {
       ECTrace.exit(ECTraceIdentifiers.COMPONENT_SERVER, CLASSNAME,
        METHODNAME, "exception, request rolledback");
       throw new RollbackRequestException(e);
      }
    }
    ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, this.getClass()
     .getName(), METHODNAME);
    return respProp;
  }

  protected boolean isValidationEnabled() {
    return true;
  }

  /*
   * This method must be overridden in subclass. This method is called
   * if no method match can be found for submit action. 
   * @param reqProp - the request properties 
   * @param ctx - the command context 
   * @param form - the action form
   * @param submitAction - the submit action 
   * @return response properties 
   * @throwsECException if unknown action handler fails
   */
  public TypedProperty unknownAction(TypedProperty reqProp,
    CommandContext ctx, XICBaseActionForm form, String submitAction)
    throws ECException {
   return null;
  }

  /*
   * Method to tell submit framework to clear request properties after
   * any and all action handler methods are executed. Default 
   * behaviour is not to clear request properties 
   * @return false by default. Override to return true if needed.
   */
  public boolean clearRequestPropertiesForAll() {
    return false;
  }//end doClearRequestProperties

  /*
   * This method can be called from any method in subclass in order to
   * tell submit framework to clear request properties after submit 
   * action handler method completes. This method allows finer grained
   * control over which action handlers clear request properties. 
   * @param value - true if wnat to clear request properties, 
   * false if not.
   */
  protected void setClearRequestProperties(boolean value) {
    this.clearReqProp = value;
  }

  /*
   * Returns whether request properties are set to be cleared or not 
   * @return status of request property clear
   */
  protected boolean getClearRequestProperties() {
    return this.clearReqProp;
  }

  /*
   * This moves common fields request property fields to response 
   * properties, and clears the request properties. This is done so 
   * that krypto in redirect is kept short (i.e. IE limitation on 
   * parameter length)
   */
  protected void clearRequestProperties(
      TypedProperty requestProperties) {
    if (requestProperties == null) {
      return;
    }//end if
    ArrayList propsToKeep = new ArrayList();
    propsToKeep.add("storeId");
    propsToKeep.add("langId");
    propsToKeep.add("catalogId");
    propsToKeep.add("categoryId");
    Map tempReqProperties = new TypedProperty();
    for (int x = 0; x < propsToKeep.size(); x++) {
     if (requestProperties.getString((String) propsToKeep.get(x),
       null) != null
       && requestProperties.getString((String) propsToKeep.get(x),
        null) != null) {
      tempReqProperties.put((String) propsToKeep.get(x),
       requestProperties.getString((String) propsToKeep.get(x),
                ""));
      }//end if
    }//end for
    requestProperties.keySet().clear();
    requestProperties.putAll(tempReqProperties);
  }//end clearRequestProperties

  /*
   * Invokes the submit action @param method - the method instance 
   * that we are to invoke @param form - action form that we are to 
   * pass to submit action handler method 
   * @param submitAction - the name of the submit action 
   * @throws ECException 
   * @return response properties
   */
  private Map invokeMethod(Method method, XICBaseActionForm form,
    String submitAction, ViewCommandContext vContext,
    RequestHandle handle) throws ECException {
   Object[] args = new Object[5];
   args[0] = vContext.getRequestProperties();
   args[1] = vContext;
   args[2] = form;
   args[3] = submitAction;
   args[4] = handle;
   try {
    return (Map) method.invoke(getSubmitActionHandlerInst(), args);
   } catch (InvocationTargetException e) {
     form.setSubmitAction("");
    if (e.getTargetException() instanceof ECApplicationException) {
     throw (ECApplicationException) e.getTargetException();
   } else if (e.getTargetException() instanceof ECException) {
     throw (ECException) e.getTargetException();
   } else {
     throw new ECApplicationException(e);
     }
   } catch (IllegalAccessException f) {
     throw new ECApplicationException(f);
   }
  }//end invokeMethod

  /*
   * Gets the Java Method that we are to invoke through reflection 
   * invocation
   * @param handlerInstance - the class that contains the submit 
   * action method that we are to invoke 
   * @param submitAction - the submit action 
   * @return the Java Method that represents the submit action handler
   */
  private Method getMethod(Object handlerInstance, 
      String submitAction) {
    Class[] params = new Class[5];
    params[0] = TypedProperty.class;//Request Properties
    params[1] = CommandContext.class;//The command context
    params[2] = XICBaseActionForm.class;//The action form
    params[3] = String.class;//The submit action (for validation if
    // needed)
    params[4] = RequestHandle.class;
    try {
     return handlerInstance.getClass().getMethod(submitAction,
      params);
    } catch (NoSuchMethodException e) {
      //Call unknownAction handler
      try {
       return handlerInstance.getClass().getMethod("unknownAction",
        params);
      } catch (NoSuchMethodException f) {
        return null;
      }//end catch
    }//end catch
  }//end geMethod

  /*
   * This method determines what the submit action was 
   * @param form - the action form that was submitted with the action 
   * @return the submit action string
   */
  private String getAction(XICBaseActionForm form,
    ViewCommandContext vContext) {
   if (GenericValidator.isBlankOrNull(form.getSubmitAction())) {
    Pattern p = Pattern.compile("[A-Za-z0-9]+\\.x");//Submit through
                                                    //input type=image
     Matcher m;
     Enumeration enum = vContext.getRequestProperties().keys();
     String param;
     while (enum.hasMoreElements()) {
     param = (String) enum.nextElement();
      m = p.matcher(param);
     if (m.matches()) {
      //We found submit action
       form
        .setSubmitAction(param.substring(0,param.length() - 2));
        break;
      }//end if
      }//end while
    }//end if
    return form.getSubmitAction();
  }//end getAction

  /*
   * Retrurns the class instance that contains the action handler
   * method 
   * @return this (since everyone's submit action handler class should
   * extend XICSubmitBaseAction.java)
   */
  private Object getSubmitActionHandlerInst() {
    return this;
  }
}

XICExpressOrderFormSubmitAction

In XICExpressOrderFormSubmitAction (Listing 5), four submit action handler methods are defined, including addToCart(), addToWishList(), addLines(), and unknownAction(). Each of these methods performs the corresponding logic when different submit buttons and actions are triggered on the express order form page. For example, addToCart() calls the business layer controller command OrderItemAdd and addToWishList() calls InterestItemAddCmd. clearRequestPropertiesForAll(), and returns true, indicating that requestProperties is cleared after each handler method.

Listing 5. XICExpressOrderFormSubmitAction code
public class XICExpressOrderFormSubmitAction extends
    XICBaseSubmitAction {
  public boolean clearRequestPropertiesForAll() {
    return true;
  }//end clearRequestPropertiesForAll

  public Map unknownAction(TypedProperty reqProp, CommandContext ctx,
      XICBaseActionForm form, String action, RequestHandle handle)
      throws ECException {
    final String METHODNAME = "unknownAction";
    ECTrace.entry(ECTraceIdentifiers.COMPONENT_EXTERN, this
        .getClass().getName(), METHODNAME);
    Map respProp = new HashMap();
    XICExpressOrderForm orderForm = (XICExpressOrderForm) form;

    //ASSERT: User is in unknownAction because he/she clicked clicked
    // refresh
    // when /SubmitExpressOrder was url
    //Go to generic error view.
    throw new ECApplicationException(ECMessage._ERR_GENERIC, this
        .getClass().getName(), METHODNAME);
  }//end unknownAction

  /*
   * Handles adding order form line items to current cart. @param
   * reqProp - request properties @param ctx - command context @param
   * form - Action form @param action - The action that submitted the
   * action form @throws ECException @return Final response properties
   */
  public Map addToCart(TypedProperty reqProp, CommandContext ctx,
     XICBaseActionForm form, String action, RequestHandle handle)
     throws ECException, RequestException {
    final String METHODNAME = "addToCart";
    ECTrace.entry(ECTraceIdentifiers.COMPONENT_EXTERN, this
     .getClass().getName(), METHODNAME);
    Map respProp = new HashMap();
    XICExpressOrderForm orderForm = (XICExpressOrderForm) form;

    //Case: No existing or new items on order form
    if (orderForm.getNumNonBlankOrderItems() <= 0) {
      ECTrace
       .trace(ECTraceIdentifiers.COMPONENT_EXTERN, this.getClass()
         .getName(), METHODNAME,
       "No Items to add/checkout... forwarding back to order form");
      respProp
       .put(
         ECConstants.EC_VIEWTASKNAME,
         XICStrutsConstants.
         ACTION_FORWARD_EXPRESSORDERSUBMIT_EXPRESSORDERFORM);
      ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, this
        .getClass().getName(), METHODNAME);
      return respProp;
    }//end if

    //ASSERT: Update was successful
    TypedProperty newTP = orderForm.serializeOrderItems();
    //Case: No new order items to add/checkout
    if (newTP.size() <= 0) {
      //We only updated existing items, so just redirect to display
      // cart or
      // checkout page
      ECTrace.trace(ECTraceIdentifiers.COMPONENT_EXTERN, this
       .getClass().getName(), METHODNAME,
       "No new items to add to order");
      ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, this
       .getClass().getName(), METHODNAME);
      return respProp;
    }//end if
    clearRequestProperties(ctx.getRequestProperties());
    newTP.putAll(ctx.getRequestProperties());
    try {
     respProp = WebControllerHelper.executeCommand(handle, newTP,
      com.ibm.commerce.orderitems.commands.OrderItemAddCmd.NAME);
     respProp.put(ECConstants.EC_REDIRECTURL,
      "OrderCalculate?URL=OrderItemDisplay");
     respProp.put(ECConstants.EC_VIEWTASKNAME,
      ECConstants.EC_GENERIC_REDIRECTVIEW);
    } catch (RequestException e) {
      if (e.getOriginalException().getECMessage().equals(
       ECMessage._ERR_PROD_NOT_EXISTING)) {
       e.getOriginalException().setErrorTaskName(
       XICStrutsConstants.
       ACTION_FORWARD_EXPRESSORDERSUBMIT_EXPRESSORDERFORM);
       throw e.getOriginalException();
     } else {
       throw e;
      }
    }
    return respProp;
  }//end addToCart

  /*
   * Handles adding order form line items to current cart. @param
   * reqProp - request properties @param ctx - command context @param
   * form - Action form @param action - The action that submitted the
   * action form @throws ECException @return Final response properties
   */
  public Map addToWishList(TypedProperty reqProp, CommandContext ctx,
      XICBaseActionForm form, String action, RequestHandle handle)
      throws ECException, RequestException {
    final String METHODNAME = "addToWishList";
    ECTrace.entry(ECTraceIdentifiers.COMPONENT_EXTERN, this
        .getClass().getName(), METHODNAME);
    Map respProp = new HashMap();
    XICExpressOrderForm orderForm = (XICExpressOrderForm) form;

    //Case: No existing or new items on order form
    if (orderForm.getNumNonBlankOrderItems() <= 0) {
      ECTrace.trace(ECTraceIdentifiers.COMPONENT_EXTERN, this
          .getClass().getName(), METHODNAME,
          "No Items to add forwarding back to order form");
      respProp.put(ECConstants.EC_VIEWTASKNAME,
      XICStrutsConstants.
      ACTION_FORWARD_EXPRESSORDERSUBMIT_EXPRESSORDERFORM);
      ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, this
          .getClass().getName(), METHODNAME);
      return respProp;
    }//end if

    //ASSERT: Update was successful
    TypedProperty newTP = orderForm.serializeOrderItems();
    //Case: No new order items to add/checkout
    if (newTP.size() <= 0) {
      //We only updated existing items, so just redirect to display
      // cart or
      // checkout page
      ECTrace.trace(ECTraceIdentifiers.COMPONENT_EXTERN, this
          .getClass().getName(), METHODNAME,
          "No new items to add to wish list");
      ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, this
          .getClass().getName(), METHODNAME);
      return respProp;
    }//end if
    clearRequestProperties(ctx.getRequestProperties());
    newTP.putAll(ctx.getRequestProperties());
    try {
      respProp = WebControllerHelper
          .executeCommand(
             handle,
             newTP,
             com.ibm.commerce.interestitems.commands.
             InterestItemAddCmd.NAME);
      respProp.put(ECConstants.EC_REDIRECTURL, "InterestItemDisplay");
      respProp.put(ECConstants.EC_VIEWTASKNAME,
          ECConstants.EC_GENERIC_REDIRECTVIEW);
    } catch (RequestException e) {
      if (e.getOriginalException().getECMessage().equals(
          ECMessage._ERR_CREATE_OR_UPDATE_IITEM)) {
        throw new ECApplicationException(
            ECMessage._ERR_CREATE_OR_UPDATE_IITEM,
            "com.ibm.commerce.struts.xic.webstore.commands." +
            "XICExpressOrderFormSubmitCmdImpl",
            METHODNAME,
            XICStrutsConstants.
            ACTION_FORWARD_EXPRESSORDERSUBMIT_EXPRESSORDERFORM);
      } else {
        throw e;
      }
    }
    return respProp;
  }//end addToWishList

  /*
   * Handles adding additional blank lines to order form. @param
   * reqProp - request properties @param ctx - command context @param
   * form - Action form @param action - The action that submitted the
   * action form @throws ECException @return Final response properties
   */
  public Map addLines(TypedProperty reqProp, CommandContext ctx,
      XICBaseActionForm form, String action, RequestHandle handle)
      throws ECException {
    final String METHODNAME = "addLines";
    ECTrace.entry(ECTraceIdentifiers.COMPONENT_EXTERN, this
        .getClass().getName(), METHODNAME);
    Map respProp = new HashMap();
    XICExpressOrderForm orderForm = (XICExpressOrderForm) form;
    orderForm.addNBlankLines(Integer.parseInt(orderForm
        .getLinesToAddSelection()));
    //orderForm.populateDropDowns(ctx);
    respProp
        .put(
            ECConstants.EC_VIEWTASKNAME,
            XICStrutsConstants.
            ACTION_FORWARD_EXPRESSORDERSUBMIT_EXPRESSORDERFORM);
    ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, this.getClass()
        .getName(), METHODNAME);
    return respProp;
  }//end addLines
}

Defining XICExpressOrderFormSubmitAction in the Struts config file

In struts-config-experssOrderForm.xml, the express form submit action is defined as shown in Listing 6.

Listing 6. Define a submit action in the Struts config file

Click to see code listing

Listing 6. Define a submit action in the Struts config file

<action path="/ExpressOrderFormSubmit" type="com.ibm.commerce.struts.xic.webstore.actions.XICExpressOrderFormSubmitAction" 
name="XICExpressOrderForm" parameter="NoCommand" scope="request" validate="false" 
input="ExpressOrderFormSubmit_ErrorView">
  <forward name="ExpressOrderFormSubmit_expressOrderForm" 
   path="/servlet/ExpressOrderFormDisplay" 
   className="com.ibm.commerce.struts.ECActionForward">
	<set-property property="properties" value="storeDir=no"/>
  </forward>
  <forward name="ExpressOrderFormSubmit_ErrorView"
   path="/servlet/ExpressOrderFormDisplay"
   className="com.ibm.commerce.struts.ECActionForward">
     <set-property property="properties" value="storeDir=no"/>
  </forward>
</action>

Notice that in the <action> tag, the type attribute is the actual submit action class XICExpressOrderFormSubmitAction. The parameter attribute is set to NoCommand. This is because our submit action class extends the WebSphere Commerce BaseAction, which treats an action as a view and tries to forward directly to a JSP if the parameter attribute is not defined. Method BaseActioninvokeService() uses the parameter value as the controller command to be invoked. Since we overrode the base invokeService() method, the parameter value is no longer important and NoCommand is just a placeholder to avoid the action being treated as a view. The name attribute defines XICExpressOrderForm as the ActionForm for this action. The input attribute defines the error view to forward when the ActionForm validation fails. This leads us to the following section that describes the ActionForm extension.


Extending the Struts ActionForm

The following section describes how to extend the Struts ActionForm class to handle multiple submit actions on a single JSP page more easily. The sample code from XICExpressOrderForm shows how to handle the form validation errors.

XICBaseActionForm

XICBaseActionForm class (Listing 7) extends the Struts ActionForm. You can use it to hold common logic for all the ActionForms. You can think of XICBaseACtionForm as representing the view or JSP page as an object. The page can have input elements on the JSP using Struts tags that map to Java bean attributes in the XICBaseActionForm. A page can also have various actions on it.

For those of you familiar with JSF, you can have multiple buttons on a page that can submit the form to different backend methods. In this extended framework, you can achieve this feature with the attribute submitAction in Listing 7. submitAction indicates what kind of form submit is triggered when there are multiple form submit elements in the same form. In the express order form example, you can add the submit action to the shopping cart, add to wish list, or add more lines to the form.

Listing 7. XICBaseActionForm code snippet
public abstract class XICBaseActionForm extends ActionForm {
	private String submitAction = "";	

.............
	/**
	 * @return Returns the submitAction.
	 */
	public String getSubmitAction() {
		return submitAction;
	}
	/**
	 * @param submitAction The submitAction to set.
	 */
	public void setSubmitAction(String submitAction) {
		this.submitAction = submitAction;
	}
}XICExpressOrderForm

XICExpressOrderForm extends XICBaseActionForm and it contains an array list of OrderItem objects that represent the line items in the order form. It overrides the validate() method from the Struts ActionForm (Listing 8). This method contains specific validation logic for the express order form page. Because validation is enabled in XICExpressOrderFormSubmitAction, when an error occurs, the Struts framework will forward to the view specified as the input attribute in the Struts config file.

Listing 8. validate() method in XICExpressOrderForm
  public ActionErrors validate(ActionMapping map,
      HttpServletRequest request) {
    final String METHODNAME = "validate";
    ECTrace.entry(ECTraceIdentifiers.COMPONENT_EXTERN, this
        .getClass().getName(), METHODNAME);
    ActionErrors errors = new ActionErrors();

    //We do not validate for this action
    if (getSubmitAction().equals("addLines")) {
      return null;
    }//end if

    //Holds errors for each line item
    TypedProperty itemErrors;
    if (getNumNonBlankOrderItems() <= 0) {
      errors.add(FORM_ERROR, new ActionError(
          XICExpressOrderForm.BLANKFORM_ERROR, new Object[] {}));
    } else {
      //There are items to validate
      for (int index = 0; index < getOrderItems().size(); index++) {
        if (!isOrderItemBlank(index)) {
          //itemErrors = new TypedProperty();
          //Validate quantity
          validateItemQuantity(index, errors);
          //Validate part number
          validatePartNumber(index, errors);
        }//end if
      }//end for
    }//end if
    ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, this.getClass()
        .getName(), METHODNAME);
    return errors;
  }

Implementing the express order form JSP page using Struts tags

You can find the complete source code for ExpressOrderForm.jsp in the provided sample code. The following sections are highlights in the JSP.

Using Struts <html> form tags

To incorporate the ActionForm class, XICExpressOrderForm, described in the above section, in expressOrderForm.jsp, we use Struts <html:form> to define the form and <html:hidden> tags to generate the hidden fields in the form.

Listing 9. Example of using Struts <html> tags
<html:form styleId="orderForm" action="/ExpressOrderFormSubmit">
  <%-- Common hidden fields --%>
  <input type="hidden" name="storeId" value="${storeId}">
  </input>
  <input type="hidden" name="catalogId" value="${catalogId}">
  </input>
  <input type="hidden" name="langId" value="${langId}"></input>
  <%-- Common fields used by Form Submit Framework --%>
<html:hidden property="submitAction"
 styleId="submitAction"></html:hidden>
<input type="hidden" id="actionFormName"
 value="XICExpressOrderForm"></input>
  
  <%-- Express Order Form specific hidden fields --%>
  <html:hidden property="maxLines" styleId="maxLines">
  </html:hidden>
  <html:hidden property="maxItemQuantity"></html:hidden>
<html:hidden property="showLinesToAddList"
 styleId="showLinesToAddList"></html:hidden>

The generated HTML source code of the form tag is:

<form name="XICExpressOrderForm" method="post" action="/webapp/wcs/stores/
servlet/ExpressOrderFormSubmit" id="orderForm">

Notice that the name of the form is the name of the ActionForm and the

styleId becomes the ID of the form. For properties not defined in the XICExpressOrderForm class, such as storeId, we use regular <input> tags. The hidden input field submitAction is used in XICExpressOrderFormSubmitAction to determine the corresponding method to be called.

Displaying error messages using Struts ActionErrors

To display error messages, use Struts ActionErrors and its corresponding tags. The following code in Listing 10 loops through all the errors and displays the messages pulled from the storeErrorMessages properties file.

Listing 10. Example of error message display using Struts ActionErrors
<fmt:setBundle basename="${jspStoreDir}storeErrorMessages" var="exFormError" />
<logic:messagesPresent>
  <tr valign="top">
    <td width="10">&nbsp;</td>
    <td colspan="3">
      <span class="error">
      <bean:define id="actionErrors"
       type="org.apache.struts.action.ActionErrors" name=
       "<%= org.apache.struts.action.Action.ERROR_KEY %>"/>
      <c:set var="errorIter" scope="request" value=
      "<%= actionErrors.get() %>"/>
      <logic:iterate name="errorIter" id="error"
       type="org.apache.struts.action.ActionError">
        <fmt:message key="${error.key}" bundle="${exFormError}">
          <c:forEach var="arg" items="${error.values}">
            <fmt:param value="${arg}"/>
          </c:forEach>
        </fmt:message>
        <br/>
      </logic:iterate>
      </span>
    </td>
  </tr>
</logic:messagesPresent>

Displaying an array object using Struts <nested> tags

To display a list of entry boxes for quantities and part numbers, use Struts <nested:interate> tag as shown in Listing 11.

Listing 11. Example of using Struts <nested> tags
<%-- Output Line Items --%>
<c:set var="lineNum" value="${1 + 0}" scope="request"></c:set>
<%-- Forces lineNum to be Long --%> 
<nested:iterate property="orderItems" id="item">
  <tr valign="top">
    <td>
      <c:out value="${lineNum}"></c:out>
    </td>
    <td>
      <nested:text property="quantity" size="7" 
      styleClass="iform"></nested:text>
    </td>
    <td>
      <nested:text property="partNumber" size="15" maxlength="24"
      styleClass="iform"></nested:text>
    </td>                    
  </tr>
  <c:set var="lineNum" value="${lineNum + 1}" scope="request"></c:set>
</nested:iterate>

Displaying a drop down selection box using Struts tags

At the bottom of the express order form, a drop down box that allows users to add additional lines to the form is implemented using the Struts <html:select> and <html:optionsCollection> tags, as shown in Listing 12.

Listing 12. Example of using Struts tags for selection box
<%-- Add blank line items action --%>
  <label id="numOrderItems" style="display:none">${lineNum - 1}</label> 
  <nested:equal property="showLinesToAddList" value="true">
    <div id="AddLinesDiv">
    <label><fmt:message key="AddLines" bundle="${exFormBundle}"/>   
    </label> 
    <html:select property="linesToAddSelection"
      styleId="linesToAddList" styleClass="iform"
      onchange="javascript:submitForm('addLines');">
      <html:optionsCollection property="linesToAddList">   
      </html:optionsCollection>
    </html:select>
    </div>
  </nested:equal>

Using tiles

WebSphere Commerce supports Struts tiles. Another way of implementing the express order form page is provided in the sample code, ExpressOrderFormTiles.zip file. Note the following changes:

  • Add tiles plugin in the Struts config file:
      <plug-in className="org.apache.struts.tiles.TilesPlugin">
    <set-property property="definitions-config" 
    value="/WEB-INF/tiles-defs.xml" />
        <set-property property="moduleAware" value="true" />
    <set-property property="definitions-parser-validate" 
    value="true" />
    </plug-in>
  • Add tiles definition in tiles-def.xml:
    <tiles-definitions>
    <definition name="ConsumerDirect.tiles.expressOrderForm"
     path="/ConsumerDirect/tiles/pageLayout.jsp">
      	<put name="header"
            value="/ConsumerDirect/tiles/tilesHeader.jsp"/>
      	<put name="body"
            value="/ConsumerDirect/ShoppingArea/expressOrderForm2.jsp"/>
      	<put name="footer"
            value="/ConsumerDirect/tiles/tilesFooter.jsp"/>
      </definition>
    </tiles-definitions>
  • Update the path attribute for ExpressOrderFormDisplay forward:
    <forward name="ExpressOrderFormDisplay/10001"  
    path="tiles.expressOrderForm" 
    className="com.ibm.commerce.struts.ECActionForward">
    </forward>

Implementation considerations

The following section describes a few challenges we had during implementation and some limitations in the base code.

No local forward

There is no local forward support in WebSphere Commerce. In Struts, each action can have its own local forward such that the forward name only needs to be unique within its associated action tag. The WebSphere Commerce Struts framework does not support this. Every forward is treated globally, therefore a forward name has to be unique, regardless if it is in a different action tag.

Reuse common JSP fragments

In an application, often the same JSP fragment appears on multiple pages. One way of doing this is to put all the common form fields in a base ActionForm class and have the ActionForm classes of different pages extend from the base ActionForm class. In the JSP pages containing the common fragment, use the Struts nested tag instead of the HTML tags.

Error handling

There are different ways to handle errors. See the WebSphere Commerce Information Center topic, Adding the Validator plug-in. The example in this article implements the validation logic in the ActionForm class and uses Struts ActionErrors to pass messages to the JSP. When defining actions in the Struts config xml, make sure you set the validate attribute to false. Otherwise, if the ActionForm.validate() method returns errors, the Struts RequestProcessor intercepts the request before it reaches the Action class and breaks the WebSphere Commerce error handling framework. To enable validation, you can override the isValidationEnabled() method to return true.

Another way to handle errors is to follow the traditional WebSphere Commerce exception handling framework to throw ECApplicationExceptions, instead of creating ActionErrors. The JSP uses the out-of-the box ErrorDataBean and StoreErrorDataBean to display error messages. However, we found that when forwarding to an error view, since we always go through a display action class before hitting the JSP, the exception objects, although still in the request, are not set in requestProperties. Therefore, to support using ErrorDataBean and StoreErrorDataBean, in XICBaseDisplayAction, we overrode the getRequestParameters() method to retrieve the exception objects from the request and set them in requestProperties. In expressOrderForm.jsp, there is a commented out section that shows the alternative way of displaying error messages:

<%-- The following code uses commerce ErrorDataBean to display error messages. 
ErrorMessageSetup.jspf is used to retrieve an appropriate error message when there 
is an error 
  <%@ include file="../include/ErrorMessageSetup.jspf"%>
  <c:if test="${!empty errorMessage}">
    <tr>
      <td width="10">&nbsp;</td>
      <td colspan="3"><span class="error">
        <c:out value="${errorMessage}"/><br/></span>
      </td>
    </tr>
   </c:if>		  --%>

Command redirect and forward considerations

In WebSphere Commerce, forward does not carry forward parameters added or updated in requestProperties or responseProperties from one command to the next. Only the original values in requestProperties are visible in the next command. Therefore, if you need to pass additional information to the next command, you need to set it as an attribute in the request using request.setAttribute(). The command forwarded to will need to retrieve it from the request using request.getAttribute().

During a redirect, all the parameters in the requestProperties from the first command will be serialized and appended to the next command's URL. Since the URL has a limit on the length, when the number of parameters exceed a certain limit, the page being redirected to will fail to be loaded.

When setting up forward to an action, instead of to a JSP directly, in the struts-config file, add a /servlet/ prefix. For example:

<forward name="ExpressOrderFormSubmit_expressOrderForm"
  path="/servlet/ExpressOrderFormDisplay"
  className="com.ibm.commerce.struts.ECActionForward"> 
  <set-property property="properties" value="storeDir=no"/>
</forward>

Invoking a WebSphere Commerce command in Action classes

When invoking a WebSphere Commerce command in an Action class, do not use CommandFactory.createCommand() to create a command that calls cmd.execute() directly. This results in access control errors or breaks the WebSphere Commerce session information. Instead, use the following code to execute a controller command:

WebControllerHelper.executeCommand(RequestHandle handle, Map map, String interfaceName)

You can find an example in the XICExpressOrderFormSubmitAction.addToCart() method.


Tips

The following tips can help you decide when to use the extended WebSphere Commerce Struts framework or the WebSphere Commerce out-of-the-box command framework:

  • Use the extended WebSphere Commerce Struts framework if the site needs major customization of the base product and if the flow of the pages is fairly complex, such as not a simple linear flow. The Struts framework can handle the routing of the pages quite clearly and efficiently.
  • Use this framework if most of the developers on your project are experienced Struts developers. Often with big projects, many developers hired are contractors who have extensive experience in Struts, but they might not know WebSphere Commerce very well. In this situation, using the framework can decrease the learning curve of WebSphere Commerce and improve productivity.
  • Do not use this framework if the project is relatively simple and most of the work is accomplished by extending the existing base command.

Conclusion

This article described a framework that we developed to extend WebSphere Commerce by using Struts. The sample code illustrated how to implement a simple Web page using the framework. It also listed some of the issues you might run into during implementation.


Downloads

DescriptionNameSize
Code sampleExpressOrderForm.zip27KB
Code sampleExpressOrderFormTiles.zip7KB

Resources

Learn

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into WebSphere on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=WebSphere
ArticleID=349660
ArticleTitle=Customizing WebSphere Commerce by using the Struts framework
publish-date=11052008