Specifying One-to-Many Relationship in ATG Repositories

Monta driving on Flickr
(Photo: Monta driving by Yogma)

Specifying one-to-many relationships is ridiculously easy in Ruby on Rails.  Unfortunately it’s not so straight-forward in ATG repositories.

First you specify the “belongs to” relationship.  In this example the player belongs to a team.

[xml]<item-descriptor name="player">
<table name="team_players" type="auxiliary" id-column-names="team_id" shared-table-sequence="1">
<property name="team" column-name="team_id" item-type="team" />
</table>
</item-descriptor>[/xml]

Then you specify the “has many” relationship.  In this example the team has many players.

[xml]<item-descriptor name="team">
<table name="team_players" type="multi" id-column-names="player_id" shared-table-sequence="2">
<property name="players" column-name="player_id" data-type="set" component-item-type="player" />
</table>
</item-descriptor>[/xml]

Note the trick is specifying the “shared-table-sequence.”

Here is the SQL for the table that specifies this relationship in our example.

[sql]CREATE TABLE team_players
(
team_id VARCHAR2(40) NOT NULL,
player_id VARCHAR2(40) NOT NULL,
CONSTRAINT team_players_pk PRIMARY KEY (team_id, player_id),
CONSTRAINT team_players_players_fk foreign key (player_id) references players (id),
CONSTRAINT team_players_team_fk foreign key (team_id) references teams (id)
);[/sql]

Limiting the Quantity Added to a Cart

Speed Limit 14 MPH on Flickr

(Photo: Speed Limit 14 MPH by bredgur)

Sometimes the client will ask that the quantity of items you can add to the cart be limited to some number, say 14 like in the photo above. 🙂

Often people will implement this by putting in checks throughout the JSP.  But this is not the best solution because it is more labor intensive and you may miss something.

Another solution is to deal with the issue in the CartModifierFormHandler by extending the doAddItemsToOrder method.  Simply check the quantity of each AddCommerceItemInfo item and make sure that its quantity plus the quantity of the same item already in the cart does not go over the limit.  If it does modify the quantity in the AddCommerceItemInfo item appropriately.

Here is how I implemented this.

    @Override
    protected void doAddItemsToOrder(DynamoHttpServletRequest pRequest,
            DynamoHttpServletResponse pResponse) throws ServletException,
            IOException {

        // fetch the order
        Order order = getOrder();
        if (order == null) {
            String msg = formatUserMessage(MSG_NO_ORDER_TO_MODIFY, pRequest,
                    pResponse);
            throw new ServletException(msg);
        }

        // iterate through the add commerce item infos, making sure that adding
        // any of them will not result in a quantity greater than LIMIT
        AddCommerceItemInfo[] addCommerceItemInfos = getItems();
        for (int ii = 0; ii < addCommerceItemInfos.length; ii++) {

            // see if there is a commerce item already in the order for the next
            // add commerce item info
            AddCommerceItemInfo addCommerceItemInfo = addCommerceItemInfos[ii];
            String catalogRefId = addCommerceItemInfo.getCatalogRefId();
            CommerceItem commerceItem = findCommerceItemByCatalogRefId(order,
                    catalogRefId);
            if (commerceItem == null) {
                continue;
            }

            // check that the quantity we add won't result in a total quantity
            // greater than LIMIT
            long addQty = addCommerceItemInfo.getQuantity();
            long qty = commerceItem.getQuantity();
            if (qty >= LIMIT) {
                addCommerceItemInfo.setQuantity(0);
            } else if (qty + addQty > LIMIT) {
                long newAddQty = LIMIT - qty;
                addCommerceItemInfo.setQuantity(newAddQty);
            }
        }

        super.doAddItemsToOrder(pRequest, pResponse);
    }

    protected CommerceItem findCommerceItemByCatalogRefId(Order pOrder,
            String pCatalogRefId) {
        for (int ii = 0; ii < numCommerceItems; ii++) {
            CommerceItem commerceItem = (CommerceItem) commerceItems.get(ii);
            String catalogRefId = commerceItem.getCatalogRefId();
            if (catalogRefId.equals(pCatalogRefId))
                return commerceItem;
        }
        return null;
    }

Combining XML in ATG

250/365 - Bricks

(Photo: 250/365 – Bricks by Kenny Louie)

ATG uses XML files sometimes instead of Java properties files for configuration. Combining them is not as straight-forward as with Java properties files but more flexible.

The XML File Combination section in the Nucleus: Organizing JavaBean Components chapter of the ATG Programming guide gives a good explanation. You can combine XML files using one of these methods.

  • replace
  • remove
  • append
  • append-without-matching
  • prepend
  • prepend-without-matching

In most cases you will append which is what ATG does by default if the tag is not already there. If the tag is already there then ATG does a replace by default.

Note that when you do a replace you only need to specify the bare minimum. But what is the bare minimum can be tricky. Here is a case where we made a property hidden. Note that I had to specify the table type as “multi” otherwise ATG assumed it was “auxiliary” even though it’s defined as “multi” in the original XML file.
[xml]<table name="foo" type="multi">
  <property name="bar" hidden="true" />
</table>[/xml]
In another case I wanted to update the id-spaces for orders but I had to first remove before appending because replace did not work in this case. This is how I did it.
[xml]<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<id-spaces>
<id-space xml-combine="remove" name="order" seed="1" batch-size="10000" prefix="o"/>
<id-space name="order" seed="100" batch-size="1" prefix="m"/>
</id-spaces>[/xml]
To see the result of XML combining you can use the Dynamo Administration Server’s Component Browser to go to the component and then click on the property which specifies the XML file(s).

For example to find the result of combining idspaces.xml’s you would go to http://localhost:8080/dyn/admin/nucleus/atg/dynamo/service/IdGenerator/?propertyName=initialIdSpaces.

To find the result of combining productCatalogs.xml’s you would go to http://localhost:8080/dyn/admin/nucleus/atg/commerce/catalog/ProductCatalog/?propertyName=definitionFiles.

To find the result of combining commercepipeline.xml’s you would go to http://localhost:8080/dyn/admin/nucleus/atg/commerce/PipelineManager/?propertyName=definitionFile.

NullPointerException in ATG OrderDiscountCalculator

calculator on Flickr

(Photo: calculator by ansik)

There is a bug in the ATG OrderDiscountCalculator which causes a NullPointerException (NPE) under certain conditions.  Fortunately ATG provides the source for this class (I wish they did for all their classes or at least a larger subset of them) so it was pretty simple to figure out why this was happening.

The OrderDiscountCalculator assumes that the taxableShippingGroupSubtotalInfo local will not be null.  If it is an NPE will result when this local is referenced and the page that called this calculator will crash.

The simple fix is to check if it is null and if it is to continue to the next shipping group.

if (taxableShippingGroupSubtotalInfo == null) {
  continue;
}

At my request ATG Support has filed a problem report, NullPointerException in OrderDiscountCalculator.

Update 12-17-2009: ATG may have fixed this issue for ATG 9.1 p1, NPE in OrderDiscountCalculator w/empty shipping groups in Order.

ATG Date Tag Converter is Buggy

Leaf on my car on Flickr

(Photo: Leaf on my car by exfordy)

The ATG Date Tag Converter is buggy because Java’s DateFormat is inherently unsafe for multithreaded use.  This is documented in ATG PR #123210 DateTagConverter.convertObjectToString() method is not thread safe.

You run into this problem whenever you do something like this.

<dsp:valueof param=”creationDate” converter=”date” date=”M/dd/yyyy”/>

Unfortunately there is no work around.  If using the Date Tag Converter is not causing problems for you, i.e you don’t see a stack trace like below, then you can ignore this bug.

2009-04-17 00:19:08,220 ERROR [nucleusNamespace.atg.dynamo.servlet.dafpipeline.DynamoServlet] java.lang.ArrayIndexOutOfBoundsException: 502
  at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:436)
  at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2081)
  at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:1996)
  at java.util.Calendar.setTimeInMillis(Calendar.java:1071)
  at java.util.Calendar.setTime(Calendar.java:1037)
  at java.text.SimpleDateFormat.format(SimpleDateFormat.java:785)
  at java.text.SimpleDateFormat.format(SimpleDateFormat.java:778)
  at java.text.DateFormat.format(DateFormat.java:314)
  at atg.droplet.DateTagConverter.convertObjectToString(DateTagConverter.java:176)

But if you do see a bug like this then you might need to create a custom droplet to format the date.  The droplet should only use DateFormat instances that are not static.  I usually make my DateFormat instances local.

RQLQueryRange does not sort dates correctly

Roppongi District Desktop on Flickr

(Photo: Roppongi District Desktop by powerbooktrance)

This was bedeviling me for awhile.  I could not figure out why my RQLQueryRange droplet was not sorting by dates correctly.  It turns out this is a known ATG bug which only happens when you are using Oracle, which is 99% of the time.

Considering that this bug has been open since 2003 one can assume ATG is not interested in fixing it.  This is probably because there is a simple work around.

Instead of using the sortProperties parameter, put the the sort directive directly in the RQL statement in the queryRQL parameter.

<dsp:param name="queryRQL" value="ALL ORDER BY creationDate DESC"/>

The Dangers of Custom ATG Repository ID’s.

El Alma del Ebro on Flickr

(Photo: El Alma del Ebro by Paulo Brandão)

Recently I was creating some SKU and product items using the ACC and I decided to create them with custom ID’s.  The items were created fine but then I discovered I couldn’t add these items to the cart, I couldn’t use them in promotions, I couldn’t set up inventory for them, etc.  I continually got exceptions like these.

/atg/commerce/order/OrderManager	---	CONTAINER:atg.service.pipeline.RunProcessException: An exception was thrown from the context of the link named [setCatalogRefs].; SOURCE:atg.commerce.CommerceException: Unable to retrieve catalog reference for item with id ci90000002 and catalogRefId sku_free_01.
/atg/commerce/order/OrderManager		at atg.service.pipeline.PipelineChain.runProcess(PipelineChain.java:371)
/atg/commerce/order/OrderManager		at atg.service.pipeline.PipelineChainContext.runProcess(PipelineChainContext.java:185)
/atg/commerce/order/OrderManager		at atg.service.pipeline.PipelineManager.runProcess(PipelineManager.java:453)
/atg/commerce/order/OrderManager		at atg.commerce.order.OrderImpl.ensureContainers(OrderImpl.java:1261)
/atg/commerce/order/OrderManager		at atg.commerce.order.OrderImpl.getCommerceItems(OrderImpl.java:691)
/atg/commerce/order/OrderManager	....stack trace CROPPED
/atg/commerce/order/OrderManager	Caused by :atg.commerce.CommerceException: Unable to retrieve catalog reference for item with id ci90000002 and catalogRefId sku_free_01.
/atg/commerce/order/OrderManager		at atg.commerce.order.processor.ProcSetCatalogRefs.loadCatalogRef(ProcSetCatalogRefs.java:267)
/atg/commerce/order/OrderManager		at atg.commerce.order.processor.ProcSetCatalogRefs.runProcess(ProcSetCatalogRefs.java:121)
/atg/commerce/order/OrderManager		at atg.service.pipeline.PipelineLink.runProcess(PipelineLink.java:233)
/atg/commerce/order/OrderManager		at atg.service.pipeline.PipelineChain.runProcess(PipelineChain.java:343)
/atg/commerce/order/OrderManager		at atg.service.pipeline.PipelineChainContext.runProcess(PipelineChainContext.java:185)
/atg/commerce/order/OrderManager		at atg.service.pipeline.PipelineManager.runProcess(PipelineManager.java:453)
/atg/commerce/order/OrderManager		at atg.commerce.order.OrderImpl.ensureContainers(OrderImpl.java:1261)
/atg/commerce/order/OrderManager		at atg.commerce.order.OrderImpl.getCommerceItems(OrderImpl.java:691)
/atg/commerce/order/OrderManager	....stack trace CROPPED after 10 lines.

After pulling out my hair out for a few hours I realized the problem was because I had accidentally put a space at the end of the custom ID I had created. Too bad you can’t determine that from the stack trace.

Lesson Learned #1: Don’t put spaces in your custom ID’s, especially at the end.

Lesson Learned #2: Custom ID’s are a luxury you might want to avoid. 🙂

How to Add Multiple Items to the Shopping Cart in ATG

Red Cart Conga, Baby on Flickr

(Photo: Red Cart Conga, Baby by It’sGreg)

ATG’s CartModifierFormHandler has a handle method for adding multiple items to the shopping cart, handleAddMultipleItemsToOrder.

<dsp:input type="submit" bean="CartModifierFormHandler.addMultipleItemsToOrder" />

What is required is that in the request you set the product ID and SKU ID (catalogRefId) for each product you want to add.

<dsp:input bean="CartModifierFormHandler.productIds" paramvalue="product.id" type="hidden" />
<dsp:input bean="CartModifierFormHandler.catalogRefIds" paramvalue="sku.id" type="hidden" />

Seems pretty-straightforward, right?  Well there are a couple of tricks.

Trick #1: Setting the quantity

Setting the quantity of the amount of each SKU you want added to the cart requiring naming the quantity input using the SKU iD.

<input type="text" name="<dsp:valueof param="sku.id"/>" />

Trick #2: Handling zero quantity inputs

When the handleAddMultipleItemsToOrder tries to add something that has a quantity of zero or less you it will output an error message that the quantity is zero or less.  If you have a input form where the user does not have to add all the items on the page then this will be problematic.

To get around this restriction I overrode the preAddMultipleItemsToOrder method.  My method sets the productIds and catalogRefIds properties to only have items that have a quantity greater than zero.

public void preAddMultipleItemsToOrder(DynamoHttpServletRequest pReq,
    DynamoHttpServletResponse pRes) throws ServletException,
    IOException {

  // get the SKU ID's and product ID's set in the form
  String[] oldCatalogRefIds = getCatalogRefIds();
  String[] oldProductIds = getProductIds();

  // make sure that the SKU ID's and product ID's are valid and of the
  // same length
  if (oldCatalogRefIds == null || oldCatalogRefIds.length == 0)
    return;
  if (oldProductIds == null || oldCatalogRefIds.length != oldProductIds.length) {
    return;
  }

  // initialize list for the SKU ID's and product ID's that we will add to
  // the shopping cart
  List newCatalogRefIdsList = new ArrayList(
      oldCatalogRefIds.length);
  List newProductIdsList = new ArrayList(
      oldCatalogRefIds.length);

  // iterate through original SKU ID's
  for (int ii = 0; ii < oldCatalogRefIds.length; ii++) {

    // get next SKU ID
    String catalogRefId = oldCatalogRefIds[ii];

    // get quantity requested for that SKU ID
    long qty;
    try {
      qty = getQuantity(catalogRefId, pRequest, pResponse);
    } catch (NumberFormatException exc) {
      if (isLoggingDebug())
        logDebug("invalid quantity for catalogRefId=" + catalogRefId, exc);
      qty = 0;
    }

    // if quantity > 0 then save this SKU ID and it's product ID
    if (qty > 0) {
      newCatalogRefIdsList.add(catalogRefId);
      String productId = oldProductIds[ii];
      newProductIdsList.add(productId);
    }
  }

  // set the catalog ID's property to only have the SKU ID's of things
  // that are being ordered
  String[] newCatalogRefIds = new String[newCatalogRefIdsList.size()];
  newCatalogRefIdsList.toArray(newCatalogRefIds);
  if (isLoggingDebug()) {
    logDebug("old catalogRefIds=" + Arrays.toString(oldCatalogRefIds)
        + ", new catalogRefIds="
        + Arrays.toString(newCatalogRefIds));
  }
  setCatalogRefIds(newCatalogRefIds);

  // set the product ID's property to only have the product ID's of things
  // that are being ordered
  String[] newProductIds = new String[newProductIdsList.size()];
  newProductIdsList.toArray(newProductIds);
  if (isLoggingDebug()) {
    logDebug("old productIds=" + Arrays.toString(oldProductIds)
        + ", new productIds="
        + Arrays.toString(newProductIds));
  }
  setProductIds(newProductIds);
}

Update Profile in ATG Commerce

Wolf portrait 3 on Flickr
(Photo: Wolf portrait 3 by Tambako the Jaguar)

ProfileTools provides methods for updating a profile.  In ATG Commerce you can access the ProfileTools via the CommerceProfileTools.

Therefore to update a profile it is as simple as calling the update methods in ProfileTools.

getCommerceProfileTools().updateProperty(propertyName,
                                         property, getProfile());

getCommerceProfileTools().updateProperty(propertyTable, getProfile());

The update methods get the MutableRepositoryItem for the profile, set the property in the MutableRepositoryItem and then update the item in the ProfileRepository.

Pretty simple, eh? 🙂

The Strangely Behaved ATG textarea Tag

Escalera al cielo / Stairway to heaven on Flickr 
(Photo: Escalera al cielo / Stairway to heaven by Davichi)

When I create a DSP textarea tag like this it outputs the value of the bean property in the text area box.

<dsp:textarea bean="MyFormHandler.foo" rows="5" cols="20"/>

Similarly if I create a DSP textarea tag like this I get the same output.

<dsp:textarea bean="MyFormHandler.foo" rows="5" cols="20"></dsp:textarea>

However when I leave some space between the textarea opening and closing tag I get a blank instead of the value of the bean property in the text area box.

<dsp:textarea bean="MyFormHandler.foo" rows="5" cols="20">
</dsp:textarea>

Anyone know why there is a difference?

Anyway after reading the documentation it turns out if I want to control what is output in the text box I can use the default attribute.

<dsp:textarea bean="MyFormHandler.foo" rows="5" cols="20" default="bar"/>

Maybe I should have read the documentation before writing this post. 🙂

Update 2009-09-03: I also found this strange pattern of behavior with the DSP droplet tag.  When I invoked a droplet that took no parameters and had no open parameters like this:

<dsp:droplet name="MyDroplet"/>

or like this:

<dsp:droplet name="MyDroplet"></dsp:droplet>

the droplet would not be invoked.

However when I did this the droplet was invoked.  Strange …

<dsp:droplet name="MyDroplet">
</dsp:droplet>