Go to primary content
Oracle® Retail POS Suite Implementation Guide, Volume 2 – Extension Solutions
Release 14.1
E54476-02
  Go To Table Of Contents
Contents
Go To Index
Index

Previous
Previous
 
Next
Next
 

14 Oracle Retail Returns Management Extensibility Framework

The purpose of the extensibility framework is to simplify the creation and deployment of new rules and calculators. The framework uses Apache Ant to automate library extraction, code compilation, and packaging of the extensions.

The framework is packaged in the returnsExtensibility.zip file that is contained within the EPD application archive under returnsmgmt/api. To use the framework simply unzip the archive to a working directory. In most cases, you can simply update the input and output directory properties in the extensibility.properties file. If you want to compile and package the samples that are provided in the framework, all you need to do is run the build target.

Adding a New Rule

Rules get executed during the evaluateReturnRequest() method call. This section demonstrates how to create a rule that is based off of the item's technical check condition. This section also demonstrates how to make use of the MessageExtension elements.

First, make a new Evaluator class. For ease of implementation, subclass the DiscreteRuleEvaluator class. This enables you to reuse the getMatchingAction() method, which looks through the rule actions for the discrete value desired. This class figures out that discrete value.

There are only two methods to be implemented:

  • evaluate()

  • getDiscreteRuleValues()

The evaluate() method is the method that provides the meat of the Evaluator implementation. The getDiscreteRuleValues() method returns a list of string values. These strings constitute the list of valid values for this particular class. Start with a skeleton class:

package com.yourcompany.returns.rules;
 
import oracle.retail.stores.commerceservices.returns.ejb.ReturnServiceRemote;
import oracle.retail.stores.commerceservices.returns.journal.ItemAuthorizationJournalEntry;
import oracle.retail.stores.commerceservices.returns.policy.PolicyRuleDTO;
import oracle.retail.stores.commerceservices.returns.rule.RuleActionIfc;
import oracle.retail.stores.commerceservices.returns.rule.RuleProcessingException;
import oracle.retail.stores.commerceservices.returns.xml.ReturnRequestType;
 
import java.util.List;
 
/**
 * A discrete rule evaluator that checks the value of the
 * techCheckCondition extensible attribute.
 */
public class TechCheckConditionEvaluator extends DiscreteRuleEvaluator
{
 
    public RuleActionIfc evaluate(ReturnServiceRemote returnService,
            PolicyRuleDTO rule, ReturnRequestType returnRequest,
            int itemIndex, Integer rmCustomerID,
            String evaluationBusinessDate,
            ItemAuthorizationJournalEntry journalEntry)
            throws RuleProcessingException
    {
        return null;
    }
 
    public List getDiscreteRuleValues()
    {
        return null;
    }
}

Determine what the technical check condition is. This is the discrete value used to find a rule action.

In this case, parse through the returnRequest object to find the extensible value associated with this item in the request. Here is the new class, with the changes in bold:

public class TechCheckConditionEvaluator extends DiscreteRuleEvaluator
{
    public RuleActionIfc evaluate(ReturnServiceRemote returnService,
            PolicyRuleDTO rule, ReturnRequestType returnRequest,
            int itemIndex, Integer rmCustomerID,
            String evaluationBusinessDate,
            ItemAuthorizationJournalEntry journalEntry)
            throws RuleProcessingException
    {
        String techCheckReason = null;
        // find the item in the request
        ItemReturnInfo item = (ItemReturnInfo)
            returnRequest.getItemReturnInfo().get(itemIndex);
        // get the extensible attributes for the item
        MessageExtension ext = item.getMessageExtension();
        // find the techCheckCondition attribute
        if (ext != null) {
            for (Iterator it = ext.getExtensionEntry().iterator();
                it.hasNext();)
            {
                ExtensionEntry entry = (ExtensionEntry) it.next();
 
                if (entry.getName().equals("techCheckCondition")) {
                    techCheckReason = entry.getValue();
                }
            }
        }
        // return the action for this value
        return super.getMatchingAction(rule.getActions(),
            techCheckReason);
    }
 
    public List getDiscreteRuleValues() {
        return null;
    }
}

Next, ensure that this class records its actions into the journal entry, like other Evaluators do. Add a call to the journal entry. Now the class looks like the following, again with the changed portions in bold:

public class TechCheckConditionEvaluator extends DiscreteRuleEvaluator
{
    public RuleActionIfc evaluate(ReturnServiceRemote returnService,
            PolicyRuleDTO rule, ReturnRequestType returnRequest,
            int itemIndex, Integer rmCustomerID,
            String evaluationBusinessDate,
            ItemAuthorizationJournalEntry journalEntry)
            throws RuleProcessingException
    {
        String techCheckCondition = null;
        // find the item in the request
        ItemReturnInfo item = (ItemReturnInfo)
            returnRequest.getItemReturnInfo().get(itemIndex);
        // get the extensible attributes for the item
        MessageExtension ext = item.getMessageExtension();
        // find the techCheckCondition attribute
        if (ext != null) {
            for (Iterator it = ext.getExtensionEntry().iterator();
                it.hasNext();)
            {
                ExtensionEntry entry = (ExtensionEntry) it.next();
 
                if (entry.getName().equals("techCheckCondition")) {
                    techCheckCondition = entry.getValue();
                }
            }
        }
        // log the rule and result value
        journalEntry.addRuleResult(rule.getName(),
                techCheckCondition == null ? "No condition found."
                        : techCheckCondition);
        // return the action for this value
        return super.getMatchingAction(rule.getActions(),
            techCheckCondition);
    }
 
    public List getDiscreteRuleValues() {
        return null;
    }
}

Finally, fill the list of allowed values. This list is used to fill in the menu box in the Returns Management UI, for configuring the allowed values for this rule.

public class TechCheckConditionEvaluator extends DiscreteRuleEvaluator
{
    public RuleActionIfc evaluate(ReturnServiceRemote returnService,
            PolicyRuleDTO rule, ReturnRequestType returnRequest,
            int itemIndex, Integer rmCustomerID,
            String evaluationBusinessDate,
            ItemAuthorizationJournalEntry journalEntry)
            throws RuleProcessingException
    {
        // … lines removed …
    }
 
    public List getDiscreteRuleValues()
    {
        String[] values = new String[] {
                "All Tests Passed",
                "Partial Test Failure",
                "Complete Test Failure"
        };
        return Arrays.asList(values);
    }
}

A static set of strings is not the ideal implementation. The rules that are shipped with Oracle Retail Returns Management rely on parameters to define their valid values and make use of the ParameterDiscreteRuleEvaluator. This solution allows for the values to be changed independently of the class at runtime. However, this is not the only possible implementation. Since the evaluator only needs to implement the getDiscreteRuleValues() method, the implementer has a great deal of latitude in deciding how the list of strings is generated.

Now that the class is written, you need to do two things:

  1. Compile and deploy the new class in the application server. Using the extensibility framework simplifies this task.

  2. Configure the database so that we can see the new evaluator in action.

For a rule, there is only one database table to configure, the rule table (RM_RU). Table 14-1 identifies the columns in the RM_RU table.

Table 14-1 RM_RU Columns

Column Name Data Type Description Notes

ID_RU

Integer

Primary Key

Must be unique.

RU_TY_EVAL

VARCHAR

Type of Rule

One of:

  • BOOLEAN

  • DISCRETE

  • RANGE

NM_RU

VARCHAR

Display Name

NA

LU_RU_CLS

VARCHAR

Class Name

Fully qualified classname.

ID_KPI

Integer

KPI Reference

ID of associated KPI. Otherwise null.


Create a new discrete rule for the class. It does not have a KPI reference. The following SQL creates this new discrete rule:

INSERT INTO rm_ru
  (id_ru, ru_ty_eval, nm_ru, lu_ru_cls, id_kpi, fl_pnty_bx)
VALUES
  (100, 'DISCRETE', 'What is the item technical check condition?', 'com.yourcompany.returns.rules.TechCheckConditionEvaluator', null, '0');

Ensure that the rule ID is unique, and that the class name is both fully qualified and correct.

Once you have updated the database, navigate to the desired policy in the Returns Management UI. Then click the change rules/order button next to Policy Rules. You can see the new rule at the bottom of the list.

Once you click the Done button, you can click on the rule name and add actions and response codes for the various conditions you want to configure.

Figure 14-1 Example Rule Configuration Screen

Surrounding text describes Figure 14-1 .

Be sure to save the policy after configuring the rule.

The following is the complete source for the TechCheckConditionEvaluator:

package com.yourcompany.returns.rules;
 
import oracle.retail.stores.commerceservices.returns.ejb.ReturnServiceRemote;
import oracle.retail.stores.commerceservices.returns.journal.ItemAuthorizationJournalEntry;
import oracle.retail.stores.commerceservices.returns.policy.PolicyRuleDTO;
import oracle.retail.stores.commerceservices.returns.rule.RuleActionIfc;
import oracle.retail.stores.commerceservices.returns.rule.RuleProcessingException;
import oracle.retail.stores.commerceservices.returns.rule.evaluator.DiscreteRuleEvaluator;
import oracle.retail.stores.commerceservices.returns.xml.ReturnRequestType;
import oracle.retail.stores.commerceservices.returns.xml.ItemReturnInfo;
import oracle.retail.stores.commerceservices.returns.xml.MessageExtension;
import oracle.retail.stores.commerceservices.returns.xml.ExtensionEntry;
 
import java.util.List;
import java.util.Iterator;
import java.util.Arrays;
/**
 * A discrete rule evaluator that checks the value of the
 * techCheckCondition extensible attribute.
 */
public class TechCheckConditionEvaluator extends DiscreteRuleEvaluator
{
 
    public RuleActionIfc evaluate(ReturnServiceRemote returnService,
            PolicyRuleDTO rule, ReturnRequestType returnRequest,
            int itemIndex, Integer rmCustomerID,
            String evaluationBusinessDate,
            ItemAuthorizationJournalEntry journalEntry)
            throws RuleProcessingException
    {
        String techCheckCondition = null;
        // find the item in the request
        ItemReturnInfo item = (ItemReturnInfo)
            returnRequest.getItemReturnInfo().get(itemIndex);
        // get the extensible attributes for the item
        MessageExtension ext = item.getMessageExtension();
        // find the techCheckCondition attribute
        if (ext != null) {
            for (Iterator it = ext.getExtensionEntry().iterator();
                it.hasNext();)
            {
                ExtensionEntry entry = (ExtensionEntry) it.next();
 
                if (entry.getName().equals("techCheckCondition")) {
                    techCheckCondition = entry.getValue();
                }
            }
        }
        // log the rule and result value
        journalEntry.addRuleResult(rule.getName(),
                techCheckCondition == null ? "No condition found."
                        : techCheckCondition);
        // return the action for this value
        return super.getMatchingAction(rule.getActions(),
            techCheckCondition);
    }
 
    public List getDiscreteRuleValues()
    {
        String[] values = new String[] {
                "All Tests Passed",
                "Partial Test Failure",
                "Complete Test Failure"
        };
        return Arrays.asList(values);
    }
}

Adding a New KPI Calculator

Now that you have added a new rule evaluator, explore a new KPI Calculator. One of the things that a KPI can do is add to the cumulative exception count. Exceptions are triggered during the processFinalResult() method. Consider a trivial exception that simply counts up the number of times a certain item has been returned. Each time this item is returned, a row is added into the exception count.

To create a new KPI, do the following:

  1. Create the new class.

  2. Configure the database.

  3. Create some JSP fragments to manipulate the parameters for the new KPI.

The Calculator Class

First of all, you need to make a new instance of the KPICalculatorIfc interface, or more specifically a new subclass of the class BaseKPICalculator.

Because you are using the abstract class, there are three methods to implement:

  • initialize() – Called on each KPI Calculator to set up an initial state. In practice, this usually means parsing KPI instance parameters or getting a reference to the KPIService from the EJB Handle passed in. The abstract base class has a method to achieve the latter called initializeServiceFromHandle().

  • hasMatchingBehavior() – This method enables the KPIValueDTO to indicate, ex-post-facto, if the KPI was fired for a particular return ticket. Does this return match the behavior that I'm looking for? This method must not rely on any values computed by the calculate method because that method might not be called depending on the calculator type.

  • calculate() – This is the workhorse method of the class. Calculate is sent in a set of facts, using the Map object, with which it can make a decision or use to perform some kind of count of historical data. This decision or count is then returned as a BigDecimal.

For this simple class, first make a skeletal stub class:

package com.yourcompany.returns.kpis;
 
import oracle.retail.stores.commerceservices.returns.kpi.CalculatorException;
import oracle.retail.stores.commerceservices.returns.kpi.KPIInstanceIfc;
import oracle.retail.stores.commerceservices.returns.kpi.impl.BaseKPICalculator;
import oracle.retail.stores.commerceservices.returns.ticket.ReturnTicketDTO;
 
import javax.ejb.Handle;
import java.math.BigDecimal;
import java.util.Map;
 
/**
 * A KPI Calculator used for exception tracking purposes.
 */
public class SpecificItemCalculator extends BaseKPICalculator
{
 
    public void initialize(Handle handle, KPIInstanceIfc kpiInstance)
            throws CalculatorException
    {
    }
 
    public BigDecimal calculate(Map params)
    {
        return null;
    }
 
    public boolean hasMatchingBehavior(ReturnTicketDTO returnTicket)
            throws CalculatorException
    {
        return false;
    }
}

Now we need to fill in some blanks. Tackle the calculate() method first. In most cases the calculator accesses the database to answer this question. For this example KPI that only counts exceptions, this method returns a default zero value.

The map passed in contains the return information. It is somewhat awkward to withdraw data from the non-type-safe map. The map is populated with default values depending on the context in which the KPI is called. Generally, two values are populated:

    mapIDs.put(KPIParameterIfc.CUSTOMER_ID, rmCustomerID);
    mapIDs.put(KPIParameterIfc.RETURN_TICKET, returnTicket);

However, when the KPI is called from the KPIRangeEvaluator or the KPICashierRangeEvaluator, then only the customer or cashier, respectively, is populated.

Each KPI also might have instance parameters that correspond to this particular KPI. In this case, the instance parameters can be withdrawn during the initialization phase. The parameters are part of the kpiInstance class and can be accessed by the method getInstanceParameters(). This method returns a TreeSet rather than a map, though order is not important here. The set contains a group of KPIInstanceParameterDTO classes. Using the getName() method, a KPI can determine which of these parameters satisfy which criteria.

In this case, determine if a certain item is in a return ticket. First, pull the item from the KPI instance parameters. To do this, define a new constant for the parameter name and get it out of the parameters passed in to the initialize function.

    protected String itemID = null;
    public static final String PARAM_ITEM_ID = "itemID";
 
    public void initialize(Handle handle, KPIInstanceIfc kpiInstance)
            throws CalculatorException
    {
        if (! isInitialized()) {
            TreeSet kpiParams = kpiInstance.getInstanceParameters();
            Iterator iter = kpiParams.iterator();
            while (iter.hasNext())
            {
                KPIInstanceParameterDTO param = 
                        (KPIInstanceParameterDTO) iter.next();
                String paramName = param.getName();
                if (paramName.equals(PARAM_ITEM_ID))
                {
                    itemID = param.getInstanceValue();
                    break;
                }
            }
 
            setInitialized(true);
        } else {
            throw new IllegalStateException("Initializing a " +
                "calculator which is already initialized");
        }
    }

Notice that two variables are added: a constant to indicate the parameter (used later to update the SQL, and in the JSP) and an instance variable to hold the value being sought.

Add in the simplified calculate() method. In this case, return a default value of zero.

    public BigDecimal calculate(Map params)
    {
        return BigDecimalConstants.ZERO;
    }

Because we are only interested in exception counting, the significant part of the code goes in the hasMatchingBehavior() method.

    public boolean hasMatchingBehavior(ReturnTicketDTO returnTicket)
            throws CalculatorException
    {
        for (Iterator it = returnTicket.getReturnTicketItems()
                .iterator(); it.hasNext();)
        {
            ReturnTicketItemDTO item = (ReturnTicketItemDTO) it.next();
            if (item.getItemID().equals(itemID))
            {
                return true;
            }
        }
        return false;
    }

Now the class is ready. Compile and deploy the new class.

The following is the complete source for the SimpleItemCalculator:

package com.yourcompany.returns.kpis;
 
import oracle.retail.stores.commerceservices.returns.kpi.CalculatorException;
import oracle.retail.stores.commerceservices.returns.kpi.KPIInstanceIfc;
import oracle.retail.stores.commerceservices.returns.kpi.KPIInstanceParameterDTO;
import oracle.retail.stores.commerceservices.returns.kpi.impl.BaseKPICalculator;
import oracle.retail.stores.commerceservices.returns.ticket.ReturnTicketDTO;
import oracle.retail.stores.commerceservices.returns.ticket.ReturnTicketItemDTO;
import oracle.retail.stores.common.utility.BigDecimalConstants;
 
import javax.ejb.Handle;
import java.math.BigDecimal;
import java.util.Map;
import java.util.TreeSet;
import java.util.Iterator;
/**
 * A KPI Calculator used for exception tracking purposes.
 */
public class SpecificItemCalculator extends BaseKPICalculator
{
    protected String itemID = null;
    public static final String PARAM_ITEM_ID = "itemID";
 
    public void initialize(Handle handle, KPIInstanceIfc kpiInstance)
            throws CalculatorException {
        if (! isInitialized()) {
            TreeSet kpiParams = kpiInstance.getInstanceParameters();
            Iterator iter = kpiParams.iterator();
            while (iter.hasNext())
            {
                KPIInstanceParameterDTO param =
                        (KPIInstanceParameterDTO) iter.next();
                String paramName = param.getName();
                if (paramName.equals(PARAM_ITEM_ID))
                {
                    itemID = param.getInstanceValue();
                    break;
                }
            }
 
            setInitialized(true);
        } else {
            throw new IllegalStateException("Initializing a " +
                "calculator which is already initialized");
        }
    }
 
    public BigDecimal calculate(Map params)
    {
        // exception counting only KPI
        return BigDecimalConstants.ZERO;
    }
 
    public boolean hasMatchingBehavior(ReturnTicketDTO returnTicket)
            throws CalculatorException
    {
        for (Iterator it = returnTicket.getReturnTicketItems()
                .iterator(); it.hasNext();)
        {
            ReturnTicketItemDTO item = (ReturnTicketItemDTO) it.next();
            if (item.getItemID().equals(itemID))
            {
                return true;
            }
        }
        return false;
    }
}

Database Configuration

The next step is to update the database. In this case, update two tables. These tables are the KPI table (RM_KPI) and the KPI parameter table (RM_KPI_PRMR).

Table 14-2 identifies the six columns to update in the KPI table.

Table 14-2 RM_KPI Columns

Column Name Data Type Description Notes

ID_KPI

Integer

Primary Key

Must be unique

NM_KPI

VARCHAR

Logical Name

Displayed in exception lists

DE_DISP_NM

VARCHAR

Display Name

Name displayed in the UI during user configuration

NM_KPI_CLS

VARCHAR

Class Name

Fully qualified name

CAT_KPI

Integer

KPI Category

One of:

  • Customer

  • Cashier

  • Store

  • Item

  • A combination of the above

TY_KPI

Integer

KPI Type

NA


The KPI type column is an integer value in the database. The value of this column, however, is interpreted as a series of bit flags. That is, a value of 7 in one of these fields means that the first three flags are set while the others are all blank (for example, DEC 7 = BIN 0111).

Table 14-3 identifies the three possible KPI type values.

Table 14-3 KPI Type Flags

Flag Value Flag Type

1

Rule Evaluation

4

Cumulative Exception Count

8

Alert


Types determine when a KPI is evaluated. A rule needs to have the rule evaluation type flag set.

Categories affect when a KPI is executed during the scoring phase, if it's executed at all. Customer and Cashier KPIs, for example, are executed in two different routines.

Note that the numeric values for both the categories and the types are defined in the interface KPIIfc.

For this class, create a new customer KPI that is used during cumulative exception counting. The SQL to do this looks like the following:

INSERT INTO rm_kpi 
  (id_kpi, nm_kpi, de_disp_nm, nm_kpi_cls, cat_kpi, ty_kpi)
VALUES
  (100, 'Specific Item KPI', 'When a specific item is returned', 'oracle.retail.stores.commerceservices.returns.kpi.impl.SpecificItemCalculator', 1, 4);

Make sure that the ID is unique and that the class name is both fully qualified and correct.

The next table to update is the RM_KPI_PRMR table. In this case, add in a parameter for the specific item being searched for.

Table 14-4 identifies the columns in the RM_KPI_PRMR table.

Table 14-4 RM_KPI_PRMR Columns

Column Name Data Type Description Notes

ID_KPI_PRMR

Integer

Primary Key

Must be unique

ID_KPI

Integer

Foreign Key

Non-null

NM_PRMR

VARCHAR

Parameter Name

Used to identify parameters from the getInstanceParameters() method

TY_PRMR_VAL

Integer

Data Type

  • Integer

  • String

  • Boolean

  • Date

  • List

DE_DFLT_VAL

VARCHAR

Default Value

NA

FL_CFG_PRMR

CHAR

Configurability Flag

  • 0 – configurable

  • 1 – not configurable

TY_PRMR

Integer

Usage Type

Bit flag with same scheme as TY_KPI


Most of these are obvious. One important note is that the NM_PRMR value here is what is used in the KPI method to extract the parameter. So the name in the database must match the name that you have coded. In this case, that name is itemID.

The last two values also bear discussion. The configurable flag, though set in the database, has no effect on where the KPI appears in the exceptions to track screen. The UI determines if a return activity KPI is configurable by counting the number of parameters associated with it. If there are one or more, then the KPI is configurable. If zero, then the KPI is not configurable. If the KPI is used for rules corresponding to a rule, this flag controls if the parameter should be displayed on the Edit Return Policy Rule page. If the parameter is not already in use by other KPIs, additional customization in ruleKPICfg.jsp might be necessary to have the parameter appear in the rule editor.

Finally, the TY_PRMR method is another bit field. The idea is that the same KPI can use different parameters in different situations. This field allows the developer to set at which times which parameters will be used.

The following SQL creates an exception tracking only KPI:

INSERT INTO rm_kpi_prmr
  (id_kpi_prmr, id_kpi,  nm_prmr, ty_prmr_val, de_dflt_val, fl_cfg_prmr, ty_prmr)
VALUES
  (1000, 100, 'itemID', 2, '1234', '1', 4);

Now start the app server and see the new KPI in action by going to Returns --> Configuration --> Exceptions to Track --> Customer.

Figure 14-2 Example Customer KPI Screen

Surrounding text describes Figure 14-2 .

Creating the JSP

To display something here, the application does one of the following:

  • The JSP tag class BaseKPITag tries to find a JSP along the path of /returns/kpi/classname.jsp, where classname is the non-qualified name of the class. In this case, the JSP is named SpecificItemCalculator.jsp.

  • If the JSP tag class BaseKPITag cannot find a file of this name it then checks for /returns/kpi/extensions/classname.jsp.

  • If the JSP tag class BaseKPITag cannot find the JSP there, the JSP tag class BaseKPITag defaults to using the display name value from the KPI and displays it using the JSP /returns/kpi/displayName.jsp. That is what is occurring here.

To remedy this, create and deploy a new SpecificItemCalculator.jsp. When creating this JSP, consider the following:

  • The JSP is responsible for formatting the remaining table cells for the row.

  • The JSP itself is responsible for enabling and disabling the KPI.

  • The JSP needs to create an Add button if necessary.

  • The JSP needs to create a Remove button if necessary.


    Note:

    Add and Remove buttons are used only in the case of cloneable KPIs.

  • The JSP is required to provide role-based security.

First, make a simple skeletal file. The first thing to know is that the enclosing JSP expects the JSP in the configurable section to contain three table cells (for example, <TD> elements). Create a new file that looks like the following:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="/WEB-INF/struts-nested.tld" prefix="nested" %>
 
<td width="83%" height="20" class="normal">
New content here!
&nbsp;
</td>
 
<td width="5%" height="20" class="normal">
<!-- place holder for an add button -->
&nbsp;
</td>
 
<td width="5%" height="20" class="normal">
<!-- place holder for a delete button-->
&nbsp;
</td>

Save this as /returns/kpi/extensions/SpecificItemCalculator.jsp, redeploy, and now see something a little better looking:

Figure 14-3 Example Customer KPI Screen, continued

Surrounding text describes Figure 14-3 .

Now that the page is ready, add in some editable value to it. In this case, that value is the item ID to track.

The JSP makes use of the nested functionality of the Struts framework. By the time you are in this JSP, you are already nested inside the specific parameter. By continuing to use this idiom, the parameter updates painlessly.

The JSP now looks like the following, with changes in bold:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="/WEB-INF/struts-nested.tld" prefix="nested" %>
 
<%@ page import="oracle.retail.stores.commerceservices.returns.kpi.impl.SpecificItemCalculator" %>
 
<td width="83%" height="20" class="normal">
    <nested:iterate property="allParams"
                    id="viewKpiParam"
                    type="oracle.retail.stores.webmodules.returns.app.kpi.ViewKpiParameterBean">
        <nested:nest property="kpiParam">
            <nested:equal property="name"
                          value="<%= SpecificItemCalculator.PARAM_ITEM_ID %>">
                <!-- include I18N message -->
                Enter the item ID here:
                <!-- include enabled stuff -->
                <nested:text property="value"
                             value="<%=viewKpiParam.getValue()%>"/>
            </nested:equal>
        </nested:nest>
    </nested:iterate>
</td>
<td width="5%" height="20" class="normal">
<!-- place holder for an add button -->
&nbsp;
</td>
 
<td width="5%" height="20" class="normal">
<!-- place holder for a delete button-->
&nbsp;
</td>

Notice the default item ID is displayed here in an editable text box. Notice that you can update the item ID and it now gets saved into the database.

Figure 14-4 Example Customer KPI Screen, continued

Surrounding text describes Figure 14-4 .

The code is updating properly due to the use of the nested tags. The section for the KPI looks something like this:

<input type="text"
  name="kpiCfgColl[6].kpiBean2.allParams[0].kpiParam.value”
  value="1234">

The long name, kpiCfgColl[6].kpiBean2.allParams[0].kpiParam.value, is used by Struts on the server side. It lets Struts determine which KPI bean is being talked about and update the bean accordingly.

Now that the KPI is displayed in a reasonable fashion, address security. Returns Management has a weak notion of security at the UI level. In this case, only check to see if the current user has the role necessary to modify KPIs. Check if this is a customer or cashier KPI and set the Boolean flag accordingly. Later, use this to enable or disable the input.

The following is what the JSP now looks like with the security code, with changes in bold:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="/WEB-INF/struts-nested.tld" prefix="nested" %>
 
<%@ page import="oracle.retail.stores.commerceservices.returns.kpi.KPIIfc,
                 oracle.retail.stores.commerceservices.returns.kpi.impl.SpecificItemCalculator,
                 oracle.retail.stores.webmodules.returns.ui.ReturnsConstantIfc,
                 oracle.retail.stores.webmodules.returns.ui.kpi.EditKpiForm"
        %>
<%
    EditKpiForm form = (EditKpiForm) request.getSession().getAttribute(ReturnsConstantIfc.EDIT_KPI_TAG_FORM);
    boolean disabledIfNoPrivilege = !(request.isUserInRole(ReturnsConstantIfc.EDIT_KPI_CUSTOMER_PRIVILEGE));
    if (form.getCategory() == KPIIfc.CATEGORY_CASHIER)
    {
        disabledIfNoPrivilege = !(request.isUserInRole(ReturnsConstantIfc.EDIT_KPI_CASHIER_PRIVILEGE));
    }
%>
 
<td width="83%" height="20" class="normal">
    <nested:iterate property="allParams"
                    id="viewKpiParam"
                    type="oracle.retail.stores.webmodules.returns.app.kpi.ViewKpiParameterBean">
        <nested:nest property="kpiParam">
            <nested:equal property="name"
                          value="<%= SpecificItemCalculator.PARAM_ITEM_ID %>">
                <!-- include I18N message -->
                Enter the item ID here:
                <nested:text
                        disabled="<%=disabledIfNoPrivilege%>"
                        property="value"
                        value="<%=viewKpiParam.getValue()%>"/>
            </nested:equal>
        </nested:nest>
    </nested:iterate>
</td>
 
<td width="5%" height="20" class="normal">
<!-- place holder for an add button -->
&nbsp;
</td>
 
<td width="5%" height="20" class="normal">
<!-- place holder for a delete button-->
&nbsp;
</td>

Finally, this KPI is a candidate for being cloneable. That is, being able to track returns on multiple item IDs.

In order to add in the cloning functionality, call one of two javascript functions on the page. The first is selAddSubmit(). The second is selDelSubmit(). These two methods submit the page back to the EditKpiAction class with the appropriate values to either create a clone of the selected KPI or to delete the currently selected clone.

The JSP encoding is currently a bit awkward for this functionality. The following is what the page looks like in final form when the cloneable code is added:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="/WEB-INF/struts-nested.tld" prefix="nested" %>
 
<%@ page import="oracle.retail.stores.commerceservices.returns.kpi.KPIIfc,
                 oracle.retail.stores.commerceservices.returns.kpi.impl.SpecificItemCalculator,
                 oracle.retail.stores.webmodules.returns.ui.ReturnsConstantIfc,
                 oracle.retail.stores.webmodules.returns.ui.kpi.EditKpiForm"
        %>
 
<%
    EditKpiForm form = (EditKpiForm) request.getSession().getAttribute(ReturnsConstantIfc.EDIT_KPI_TAG_FORM);
    boolean disabledIfNoPrivilege = !(request.isUserInRole(ReturnsConstantIfc.EDIT_KPI_CUSTOMER_PRIVILEGE));
    if (form.getCategory() == KPIIfc.CATEGORY_CASHIER)
    {
        disabledIfNoPrivilege = !(request.isUserInRole(ReturnsConstantIfc.EDIT_KPI_CASHIER_PRIVILEGE));
    }
 
    // please be careful with the quotation...
 
    // This is a funky work around the problem of JspC not translating the parameter inside of
    // a substitution.
    // It is a one pass, and we really need it to be a two pass like compiler.
 
    // Because the kpi.jsp page itself could contain more than one kpi with merchandise hierarchy, and the user
    // could edit any one of those, we need a way to distinguish which kpi parameter is being edited.
    String endString = new String("');");
    String comma = new String("', '");
    String callingAddCloneFunction = new String("selAddSubmit('");
    String callingDeleteCloneFunction = new String("selDelSubmit('");
%>
 
<td width="83%" height="20" class="normal">
    <nested:define id="templateKpiId" property="templateId"/>
    <nested:define id="instanceKpiId" property="instanceId"/>
 
    <nested:iterate property="allParams"
                    id="viewKpiParam"
                    type="oracle.retail.stores.webmodules.returns.app.kpi.ViewKpiParameterBean">
        <nested:nest property="kpiParam">
            <nested:equal property="name"
                          value="<%= SpecificItemCalculator.PARAM_ITEM_ID %>">
                <!-- include I18N message -->
                Enter the item ID here:
                <nested:text
                        disabled="<%=disabledIfNoPrivilege%>"
                        property="value"
                        value="<%=viewKpiParam.getValue()%>"/>
            </nested:equal>
        </nested:nest>
    </nested:iterate>
    &nbsp;
</td>
 
<td width="5%" height="20" class="normal">
    <nested:submit disabled="<%=disabledIfNoPrivilege%>"
                   onclick="<%=callingAddCloneFunction + templateKpiId + endString%>">
        <nested:message key="button.add"/>
    </nested:submit>
</td>
 
<!-- allow removal of clone if we have more than one of the same template type -->
<nested:equal property="allowedRemovalClone" value="true">
    <td width="5%" height="20" class="normal">
        <nested:submit disabled="<%=disabledIfNoPrivilege%>"
                       onclick="<%=callingDeleteCloneFunction + templateKpiId + comma + instanceKpiId + endString%>">
            <nested:message key="button.remove"/>
        </nested:submit>
    </td>
</nested:equal>
<nested:equal property="allowedRemovalClone" value="false">
    <td width="5%" class="normal">&nbsp;</td>
</nested:equal>

And, finally, see the Add and Remove buttons:

Figure 14-5 Example Customer KPI Screen, continued

Surrounding text describes Figure 14-5 .