The generic-sounding Content Assembly Mechanism, or CAM, is an exciting step beyond XML Schema, but it's new and not well documented. This article series represents CAM: The Missing Manual. This last installment is a deep exploration of CAM's ability to express exactly what you need for data-centric documents.

by Michael Sorens

Mar 16, 2009

Page 2 of 6

The Intuitive Nature of CAM Design

This section shows another example of structural variability, but puts it into practice with a complete template (see the files Birds/birdlist.cam and Birds/birdlist.xml in the downloadable code).

Consider a list of birds where, for simplicity, you are cataloguing just three types of avians: raptors, waterfowl, and passerines (songbirds). Each bird has a classification indicating its type. If a given bird is a waterfowl, then there must also be a waterfowl category to identify the bird as (for example) a diving duck or a dabbling duck. Contrariwise, if the bird is not a waterfowl then a waterfowl category must not be present. This <birdList> shows a couple of valid <bird> elements in a <birdList>:

To convert your XML into a CAM template, strip it down to remove duplication, include all choices and optional values, and substitute generic placeholders (surrounded with percent signs) for any values:

The quickest way to move this into a CAM template is to use your favorite text editor—start with an existing template skeleton and simply paste the above piece of XML into the <as:Structure> element. Here's a basic skeleton:

After you have your structure in place, you can add the formal business rules. A list of birds should obviously allow more than one bird:

<as:constraint action="makeRepeatable(//birdList/bird)" />

Each bird should be classified into one of the choices shown above for the <classification> element. The rule does not need to know what the choices are; those are defined in the structure.

<as:constraint action="setChoice(../classification/*)" />

A bird should specify one of the choices shown above for the <waterfowl-category> element.

<as:constraint action="setChoice(../waterfowl-category/*)" />

Finally, to set up the more complicated condition given above, start by stating that the <waterfowl-category> must universally not exist, so no condition attribute appears in this constraint:

<as:constraint action="excludeTree(//bird/waterfowl-category)" />

Then you add the condition back—but only when the current bird is a waterfowl. The precedence rules dictate that unconditional rules are applied first, then conditional rules, so this does what you need:

That completes the business rules. Simply take the five rules stated here and embed them in the <as:context> element in the skeleton. As you can see, mapping your requirements to the formal business rules is a straightforward, almost automatic, process.

Up to now, all rules have been in the default context; you can see this by examining the Rules view on any of the examples. A context is simply a mechanism for grouping multiple rules that need to satisfy a common condition. (The term condition is used here in the same sense as in the <as:constraint> examples you've seen thus far, e.g. "if the item weight exceeds 25 kg" or "if the color is green", etc.)

Figure 1. Adding Conditional Context: Context lets you apply conditionality to multiple rules, starting with the node upon which the context is based.

This next example contrives an international dialing scenario (chosen because readers are likely to be familiar with the concept) where several data elements are related and must move together. This page on International Dialing Codes provides a handy chart listing country code, international dialing prefix, and national dialing prefix for each country. This group of three variables for each country is precisely what a context can assist with. The main window in Figure 1 shows a CAM template for a customer list that includes a <key> element that specifies a country, followed by any number of <customer> elements. Each customer has an address and phone details. The rules shown in the figure are all in the default context; those are the starting point (see the file ContextAndParams/addressAndPhone_base.cam in the downloadable code). In this walk-through, you add a context for each country of interest and then add rules to the appropriate phone elements in that context.

Start by right-clicking the <key> element. Select Add New Context and the Add Context Wizard appears (see Figure 1, frame 2). Modify the settings in the wizard to match the changes shown: change the condition from mere existence to matching the string "US" and change the category from default to context. (When you click on the condition this opens yet another dialog that is not shown). Close out of the Add Context Wizard and select the <country-code> element. Open its context menu and select Add New Rule. The Add New Constraint Wizard opens (see Figure 1, frame 3). Modify the wizard settings to match the changes shown, as follows:

Change the action to restrictValues and add the value for the United States, +1.

Change the conditional from "No" to "Yes" to expose more fields for setting the condition.

Select the context that you just created, and leave the condition at "none." (You can compound the condition established by the context if you need extra complexity.)

When you close the Add New Constraint Wizard, your rule should appear attached to the <country-code> element (see the file ContextAndParams/addressAndPhone_base_incremental.cam in the downloadable code). Repeat the same step (see Figure 1, frame 3) to add rules to the <NDD-prefix> and <IDD-prefix> elements. Now you have a context for "US" phone numbers with rules attached to the three relevant elements for that context. Repeat the whole process for the "UK": return to the <key> element, add a context for the UK, and then on the three phone elements add rules with UK-specific values.

Figure 2. Multiple Rules in Multiple Contexts: This figure continues the exercise from Figure 1, creating a separate context for the US and the UK, and applying a rule to each of the three country-specific patterned elements for each context. The ItemRules view shows the two rules for the selected node, one in each context, while the Rules view at the bottom shows all the rules in both contexts.

Figure 2 shows the finished result (where you can find the values you need to add to each rule): the Rules view shows a separate context for US and for UK, with each context having a rule for each of the three phone elements. If you select any of the three elements in the Structure view (<country-code> is selected in the figure), then the ItemRules view exposes the rules tied to that specific node, one for each context (see the file ContextAndParams/addressAndPhone_without_param.cam). If you load the XML sample instance for this template (ContextAndParams/addressAndPhone.xml) and try a sample validation against this template, you should get three errors. The errors occur because the sample file defines two customers, one with US details and one with UK details. Because the sample defines the <key> element as US, all phone details need to conform to US rules. Therefore, the customer with UK details fails on all three rules. If you change the <key> to UK and revalidate, then the customer with US details fails on all three rules.

Now that you have an understanding of a CAM context, adding further layer of parameters on top of this is straightforward. If you have worked with XSLT, you may be familiar with passing parameters into a stylesheet to alter the behavior of a translation. That capability probably would have been embedded into XML Schema if it had been available at the time. CAM, being newer, did take advantage of this, and includes a mechanism for defining parameters, passing them into the engine along with your CAM template, and having the parameters alter the behavior of the validation.

Continuing from the last example, the goal here is simply to externalize the country <key>, to remove it from the file so that you can designate a target country at run time. Open the Parameters view and create a new parameter using either the Add button or the context menu. Select each field in turn to enter values:

Set the name to countryKey.

Set the default to US (no quotation marks).

Set the allowed values to the list of target countries in single quotes with vertical bar separators.

Confirm the usage defaults to global (you set either global or local via the item's context menu).

Now connect the new parameter by changing both CAM contexts to use the parameter instead of the <key> element. Select either context in the Rules view, right-click it, and select Edit Rule from the menu. Click on the condition to open the condition dialog, and change the /customerList/key to the name of the parameter with an initial dollar sign—in this case $countryKey. Close the wizard and your context labels should be updated. Furthermore, if you select one of the phone child nodes with a rule, its context should show the change as well. Repeat the same steps for the other context. Save the file and you are ready to validate (see the file ContextAndParams/addressAndPhone_with_global_param.cam).

Select "Run ( Run JCam…" to open the validation dialog. You should now see a new section in the dialog entitled Global Parameters. The default you specified should be showing: in this case, US.

Author's Note: I strongly advise specifying a default when you create a parameter—if you do not, and then fail to pick one here, you will usually get a cryptic error when you try to run.

Select whichever one you wish, either US or UK. You should achieve the same validation results simply by adjusting the global parameter that you did when you updated the <key> in the XML file.