Tuesday, October 2, 2012

Websphere Commerce Business Context service

Using BusinessContext for maintaining session variables in Websphere Commerce

Often eCommerce sites may require you to enter zip code in the beginning of the shopping session so that correct sales catalog is related and displayed to you. Also, this trend is common in grocery sites where they may want to locate the nearest physical store to your location and link all the products available in that store.  Shoppers are asked to enter the zip code usually soon after landing on home page which is then maintained in the session and all operations requiring zip code refer to this zipcode throughout user’s shopping session. Also, zip code should be invalidated if user logs out or closes the browser.
To implement this solution, usually first thing that would hit developers mind is cookies or http session. However Websphere commerce has much better way to implement this using business context which links information to user activity and spans through multiple requests and transactions. Websphere Commerce business Context manages contextual information and makes it available for business components. To read more about business context follow up this info center link.

In a design discussion meeting we decided to maintain user preferences in a business context and I was asked to implement this. I searched through info center and other websphere commerce forums but couldn’t find any straight forward steps to implement this solution.  After some struggle I managed to implement it and decided to create a blog for my future references and for others who may come across the same requirement.
To keep things simplified, I have taken an example of single value (zipcode), you may add more values as per your requirement. In this scenario shopper will be presented with dialog box soon after landing on the home page and asked to enter the zip code, which will be then stored in the business context. This example has been created on Madisons store and tested using Mozilla Firefox.
1.       Creating new business Context
The first thing is to create a new business context and register it with Websphere Commerce business context service.  This can be achieved by creating an interface which extends the OOTB com.ibm.commerce.context.base.Context.

package com.mycompany.context;

import com.ibm.commerce.context.base.Context;

public interface UserPreferencesContext extends Context{
     
       public static final String CONTEXT_NAME = "com.mycompany.context.UserPreferencesContext";
         
         /**
          *
          * @return
          */
          public String getZipCode();
         
          /**
           * @param zipCode
           */
          public void setZipCode(String zipCode);

}

Next, create an implementation class for UserPreferencesContext as given below. This class must extend the com.ibm.commerce.context.baseimpl. AbstractContextImpl, which provides life cycle methods to the context.

package com.mycompany.context;

import java.util.ArrayList;

import com.ibm.commerce.component.contextservice.ActivityData;
import com.ibm.commerce.context.base.Context;
import com.ibm.commerce.context.baseimpl.AbstractContextImpl;
import com.ibm.commerce.context.exception.BusinessContextException;
import com.ibm.commerce.datatype.PropertyHelper;

public class UserPreferencesContextImpl extends AbstractContextImpl implements UserPreferencesContext{
     
      private String zipCode;
  
    public static final String className = "com.mycompany.context.UserPreferencesContextImpl";
   
    /**
     *
     */
    public UserPreferencesContextImpl() {
      this.zipCode = "";
      setDirty(true);
    }
   
    /**
     *
     */
    public String getZipCode() {
      return this.zipCode;
    }
   
    /**
     *
     */
    public void setZipCode(String zipCode) {
      if (!(this.zipCode.equals(zipCode)))
          setDirty(true);
      this.zipCode = zipCode;
    }
   
  
    /**
     *
     */
    public String getContextName() {
      return CONTEXT_NAME;
    }
   
    /**
     *
     */
    public void initializeContext(ActivityData initData) throws BusinessContextException {
     
     
      this.zipCode = PropertyHelper.getString(initData.getMap(), "zipCode");
     
      if (this.zipCode == null) {
          this.zipCode = "";
      }
     
      setDirty(true);
    }
   
    /**
     *
     */
    public void preInvokeContext(ActivityData sessionData) throws BusinessContextException {
      boolean flag = false;
     
      String zipCode = PropertyHelper.getString(sessionData.getMap(), "zipCode");
     
      if (zipCode != null) {
          this.zipCode = zipCode;
          flag = true;
      }
      if (flag)
          setDirty(true);
    }
   
    /**
     *
     */
    public Object[] getContextAttributes() throws BusinessContextException {
      ArrayList list = new ArrayList();
      list.add(this.zipCode);
      return list.toArray();
    }
   
    /**
     *
     */
    public void setContextAttributes(String[] ctxAttrs) throws BusinessContextException {
      if ((ctxAttrs != null) && (ctxAttrs[0] != null)){
        
          this.zipCode = ctxAttrs[0];
      } else {
          this.zipCode = "";
      }
     
      setDirty(true);
    }
   
    /**
     *
     */
    public void clearContext() {
     
      this.zipCode = "";
       setDirty(true);
    }
   
    /**
     *
     */
    public boolean validate() throws BusinessContextException {
      return (this.zipCode != null);
    }
   
    /**
     *
     */
    public void copyContext(Context ctx) {
      if (ctx instanceof UserPreferencesContext) {
            UserPreferencesContext userPrefContext = (UserPreferencesContext) ctx;
          setZipCode(userPrefContext.getZipCode());
          setDirty(true);
      }
    }
   
    /**
     *
     */
    public String toString() {
      StringBuffer sb = new StringBuffer("\nCurrent State of User Preferences Context\n");
      sb.append("ZIP CODE : " + getZipCode() + "\n");
      return sb.toString();
    }

     
}

2.       Registering new Business Context
To have your new business context called by business context engine, it must be registered with Websphere Commerce Business Context service. Open BusinessContext.xml  located at WCS_Inallation_Directory\ workspace\WC\xml\config\businessContext.xml  and add following lines to it.
<!-- Business context created for storing values into context-->
  <BusinessContext ctxId="UserPreferencesContext"
                   factoryClassname="com.ibm.commerce.context.factory.SimpleBusinessContextFactory" >
    <parameter name="spiClassname" value="com.mycompany.context.UserPreferencesContextImpl" />
  </BusinessContext>

Also, make sure following line is added to Default, Store and Authoring ctxSetId’s  located towards the bottom of the file.

<InitialBusinessContext ctxId="UserPreferencesContext" createOrder="1" />


3.       Creating User Preferences Model
For the sake of illustration I have used a <div>  to make it appear as modal, however you can use more efficient and cross browser compatible modals for example Dojo or jQuery model. Create ZipCodeModalDisplay.jsp under Stores/WebContent/Madisons/ShoppingArea/CatalogSection/CategorySubsection   and add following code to it.

<style>
#overlay {
     visibility: hidden;
     position: absolute;
     left: 0px;
     top: 0px;
     width:100%;
     height:100%;
     text-align:center;
     z-index: 1000;
     background: rgb(0, 0, 0);
     background: rgba(0, 0, 0, 0.6);
    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";
}
#overlay div {
     width:300px;
     margin: 100px auto;
     background-color: #fff;
     border:1px solid #000;
     padding:75px;
     text-align:center;
    
}
body {
     height:100%;
     margin:0;
     padding:0;
}
</style>
<script>
function showOverlay() {
      el = document.getElementById("overlay");
      el.style.visibility = "visible";
}
function hideOverlay(){
el = document.getElementById("overlay");
el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible";
}
function submitFrm(){
var zipCodeSubmitFrm = document.forms["zipCodeForm"];
var zcode = zipCodeSubmitFrm.iZipCode.value;
if(null != zcode && zcode != '')
    zipCodeSubmitFrm.submit();
 else
 alert ('Please enter the zip code');
}
</script>

<html>
<head>
</head>
<title></title>
</head>
<body onload="showOverlay();">
      <div id="overlay">
                
               <div>
              <form action="PersistUserPreferencesContextCmd" method="post" name="zipCodeForm">
                <table>
                <tr><td align="center"> Enter the Zip Code </td></tr>
                <tr><td>Zip Code :</td><td><input type="text" name="iZipCode" id ="iZipCode"/></td></tr>
                <tr align="right"><td align="right"><input type="button" value="Submit" style="height: 25px;" onclick="submitFrm();"></td>
                <td align="left"><input type="button" value="Cancel" style="height: 25px;" onclick="hideOverlay();"/></td></tr>
                <tr><td colspan="2"></td></tr>
                </table>
              </form>
          </div>
            </div>
</body>
</html>

Next,open Stores/WebContent/Madisons/ShoppingArea/CatalogSection/CategorySubsection/TopCategoriesDisplay.jsp and add following piece of code just above the line
 <%@ include file="../../../include/LayoutContainerTop.jspf"%>

<c:choose> 
        <c:when test="${empty userPreferencesContext.zipCode }" >
          <%@ include file="ZipCodeModalDisplay.jsp"%>
        </c:when>
        <c:otherwise>
           <c:out value ="Zip code Value from business Context : ${userPreferencesContext.zipCode}"/>
        </c:otherwise>

This will make ZipCodeModalDisplay.jsp to appear as modal.

4.       Creating Controller Command to push values into context
 Business Contexts can retrieve the parameter values from the session data and persist into context. In the above UserPreferencesContextImpl following line will get the zipCode value from the request (in that case name of the input text box should be zipCode and not iZipCode) and push it into context.
String zipCode = PropertyHelper.getString(sessionData.getMap(), "zipCode");
However you may want to do some additional logic to input parameters before sending it into context for persistence. For example you may want to check if shopper has entered the valid zip code. A controller command is required to perform this additional logic. Otherwise, just submitting the zipCode form and redirecting back to desired view will suffice our objective. I preferred to achieve this via a controller command.
·         Create PersistUserPreferencesContextCmd and following code to it.
package com.mycompany.context;

import com.ibm.commerce.command.ControllerCommand;

public interface PersistUserPreferencesContextCmd extends ControllerCommand {

     
      static final String defaultCommandClassName = "com.mycompany.context.PersistUserPreferencesContextCmdImpl";
}

·         Create an Implementation class and add following code to this.

package com.mycompany.context;
/**
 * @author Sajjad
 */
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang.StringUtils;

import com.ibm.commerce.command.ControllerCommandImpl;
import com.ibm.commerce.datatype.TypedProperty;
import com.ibm.commerce.exception.ECException;
import com.ibm.commerce.ras.ECTrace;
import com.ibm.commerce.ras.ECTraceIdentifiers;
import com.ibm.commerce.server.ECConstants;

public class PersistUserPreferencesContextCmdImpl extends ControllerCommandImpl
            implements PersistUserPreferencesContextCmd {
      private static final String CLASSNAME = PersistUserPreferencesContextCmdImpl.class.getName();
      public static final String EMPTY_STRING ="";
      public static final String ZIP_CODE ="iZipCode";
      public static final String VIEWNAME ="StoreView";
    private static Logger logger = Logger.getLogger(CLASSNAME);
    private String zipCode = EMPTY_STRING;
  
   
    /**
     * This constructor calls the superclass constructor.
     */
    public PersistUserPreferencesContextCmdImpl() {
      super();
    }
   
    /**
     * This method is used to set the request properties
     */
    public void setRequestProperties(TypedProperty reqProperties) throws ECException {
      final String METHODNAME = "setRequestProperties()";
     
      ECTrace.entry(ECTraceIdentifiers.COMPONENT_EXTERN, CLASSNAME, METHODNAME);
      super.setRequestProperties(reqProperties);
     
      if (reqProperties.containsKey(ZIP_CODE)) {
          String zcode = reqProperties.getString(ZIP_CODE);
         
          if (StringUtils.isNotEmpty(zcode))
               this.setZipCode(zcode);
          else
            this.setZipCode(EMPTY_STRING);
      }
           
      ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, CLASSNAME, METHODNAME);
    }
   
    /**
     * To validate the parameters.
     *
     * @param
     * @return
     */
    public void validateParameters() throws ECException {
      final String METHODNAME = "validateParameters()";
     
      ECTrace.entry(ECTraceIdentifiers.COMPONENT_EXTERN, CLASSNAME, METHODNAME);
     
      super.validateParameters();
     
      /**
       *  Do zipcode validation here
       */
     
      ECTrace.exit(ECTraceIdentifiers.COMPONENT_EXTERN, CLASSNAME, METHODNAME);
    }
   
    public void performExecute() throws ECException {
      final String METHODNAME = "performExecute()";
      ECTrace.entry(ECTraceIdentifiers.COMPONENT_EXTERN, CLASSNAME, METHODNAME);
      super.performExecute();
      TypedProperty resProp = getRequestProperties();
     
      /*
       * Initialize the context and save zipcode
       */
      UserPreferencesContext userPreferencesContext = (UserPreferencesContext) getCommandContext().getContext(UserPreferencesContext.CONTEXT_NAME);
      userPreferencesContext.setZipCode((getZipCode()));
      if (logger.isLoggable(Level.FINE)) {
          logger.logp(Level.FINE, CLASSNAME, METHODNAME, "userPreferencesContext values are  :::::" + userPreferencesContext.toString());
      }
      System.out.println("userPreferencesContext values are  :::::" + userPreferencesContext.toString());
      // Setting the view name in the response properties.
      resProp.put(ECConstants.EC_VIEWTASKNAME, ECConstants.EC_GENERIC_REDIRECTVIEW);
      resProp.put(ECConstants.EC_REDIRECTURL, VIEWNAME);
     
      setResponseProperties(resProp);
    }
   

  
   
    /**
     * This method is used to get the zipcode.
     *
     * @return zipcode.
     */
    public String getZipCode() {
      return zipCode;
    }
   
    /**
     * This method is used to set the zipcode.
     *
     * @param zipCode
     */
    public void setZipCode(String zipCode) {
      this.zipCode = zipCode;
    }

}

Add following entry to Stores/WebContent/WEB-INF/struts-config-ext.xml

<action parameter="com.mycompany.context.PersistUserPreferencesContextCmd" path="/PersistUserPreferencesContextCmd" type="com.ibm.commerce.struts.BaseAction">
</action>

·         Run following quires to register your command in CMDREG table and add access control policies.

insert into CMDREG (STOREENT_ID, INTERFACENAME, DESCRIPTION, CLASSNAME, TARGET)
   values (0,'com.mycompany.context.PersistUserPreferencesContextCmd','This is a new controller command for context.','com.mycompany.context.PersistUserPreferencesContextCmdImpl','Local');
                 
                                   
INSERT INTO ACRESCGRY(ACRESCGRY_ID,RESCLASSNAME)
VALUES ((select coalesce(max(ACRESCGRY_ID),0)+1 from ACRESCGRY ),
'com.mycompany.context.PersistUserPreferencesContextCmd');


INSERT INTO ACRESGPRES(ACRESGRP_ID,ACRESCGRY_ID)
VALUES ((select ACRESGRP_ID from ACRESGRP WHERE  GRPNAME = 'AllSiteUserCmdResourceGroup'),
(SELECT  ACRESCGRY_ID FROM ACRESCGRY WHERE
RESCLASSNAME = 'com.mycompany.context.PersistUserPreferencesContextCmd'));

5.       Retrieving values from Business Context
Open Stores/WebContent/Madisons/include/JSTLEnvironmentSetup.jspf  and following lines of code towards the end of the file. Since JSTLEnvironmentSetup is getting included in almost every jsp , it would be convenient  to initialize your business context here. It would be just a matter of referring to a variable to retrieve values from the context throughout the site

<%
      CommandContext commandContext = (CommandContext) request.getAttribute(ECConstants.EC_COMMANDCONTEXT);
      UserPreferencesContext userPreferencesContext = (UserPreferencesContext)commandContext.getContext(UserPreferencesContext.CONTEXT_NAME);
%>
<c:set var="userPreferencesContext" value="<%=userPreferencesContext%>" scope="request"/>

6.       Testing your implementation
Websphere Commerce maintains the user activity data in two tables, CTXMGMT and CTXDATA. To test whether your new business is working and storing the intended data. Use the following query to verify.

select * from ctxdata;

You  should be able to see your context there with the zip code value you just entered.



Hope this helps !!!!!
Thanks
Sajjad











6 comments:

  1. excellent blog for starters creating a new business context...

    ReplyDelete
  2. Thanks for the clear explanation, this works like a charm!
    I have 1 question though: when I add more values, e.g. 'city', I get a DuplicateKeyException on the ctxdata-table. I first have to delete the rows with com.mycompany.context.UserPreferencesContext before he accepts this new value. This can cause some problems in the future when we want to add values to the UserPreferencesContext, does anyone know if it's possible to avoid this?

    ReplyDelete
  3. Good one Sajad. Can we add multiple values. Not sure if someone has tried. Ill let you know if it works.

    ReplyDelete
    Replies
    1. Karthik.. Yes you can add multiple values,follow the same steps for adding other values. Make sure you check length of the array in setContextAttributes to avoid arrayIndexOutof bound exceptions

      Delete
  4. Hi Sajad , A quick help is needed. Here I am creating the new businessContext as you mentioned here. But my flow is a BOD service. I am able to fetch the new Context in BOD command from facadeCleint call. but bot able to fetch in the corresponding handler. In Handler I am fetching with following code.But getting null in here. Also when trying to fetchh OOB context like GlobalisationContext,BaseContext these all are also giving null. Does it means we cant have any conetxt refernece here as businessContext object is allready provided in the handler.

    "sfErrorContext = (SFErrorContextImpl) com.ibm.commerce.foundation.server.services.businesscontext.ContextServiceFactory .getContextService().findContext("com.ibm.sf.commerce.common.helpers.SFErrorContext");

    ReplyDelete