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
excellent blog for starters creating a new business context...
ReplyDeleteThanks for the clear explanation, this works like a charm!
ReplyDeleteI 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?
Good one Sajad. Can we add multiple values. Not sure if someone has tried. Ill let you know if it works.
ReplyDeleteKarthik.. 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
DeleteHey awesom post, thanks man!
ReplyDeleteHi 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.
ReplyDelete"sfErrorContext = (SFErrorContextImpl) com.ibm.commerce.foundation.server.services.businesscontext.ContextServiceFactory .getContextService().findContext("com.ibm.sf.commerce.common.helpers.SFErrorContext");