Solving Typical Navigation Problems


Transformations are fairly straightforward when your source tree and result tree have pretty much the same overall structure. However, things can sometimes get pretty messy when they don't. I have on several occasions encountered application extract files from which EDI systems could not produce the desired output and vice versa. The only way around these types of problems is to write pre- or postprocessing programs to restructure the extract or import file.

Even though I've earned a few extra consulting fees by coding such auxiliary programs, I can't tell you how thrilled I am to report that I have yet to see or imagine an XSLT transformation that needed this type of preprocessing. XSLT with XPath expressions is extremely flexible and powerful. I'll go over just a couple of the common problems that are simply impossible to deal with using some conventional EDI systems.

Mapping a Flat Structure to a Hierarchy

I have written at least one preprocessing program for the very real problem of mapping a flat structure to a hierarchy. Some low-end bookkeeping systems export invoices one line item at a time, with each line item containing all the information about the invoice. Each line in the file has the same format. This is the basic organization of the files handled by our CSVToXML converter in Chapter 7. So, how do we turn something like this into a more conventional invoice with a header section and a detail section for each line item? It's really not very difficult.

Here's our source file. It's an example invoice from Chapter 7 with two line items.

Source (FlatToHierarchy.xml)
 <?xml version="1.0" encoding="UTF-8"?> <Invoice>   <InvoiceLine>     <CustomerNumber>BQ003</CustomerNumber>     <InvoiceNumber>2002041</InvoiceNumber>     <InvoiceDate>2002-11-12</InvoiceDate>     <PONumber>AZ999345</PONumber>     <DueDate>2002-12-02</DueDate>     <ShipToName>       Yazoo Grocers - NE Distribution Center     </ShipToName>     <ShipToStreet1>12 Industrial Parkway, NW</ShipToStreet1>     <ShipToCity>Portland</ShipToCity>     <ShipToStateOrProvince>ME</ShipToStateOrProvince>     <ShipToPostalCode>04101</ShipToPostalCode>     <ItemID>HCVAN</ItemID>     <ItemQuantity>12</ItemQuantity>     <UnitPrice>2.59</UnitPrice>     <ItemDescription>       Instant Hot Cocoa Mix - Vanilla flavor     </ItemDescription>     <ExtendedPrice>31.08</ExtendedPrice>   </InvoiceLine>   <InvoiceLine>     <CustomerNumber>BQ003</CustomerNumber>     <InvoiceNumber>2002041</InvoiceNumber>     <InvoiceDate>2002-11-12</InvoiceDate>     <PONumber>AZ999345</PONumber>     <DueDate>2002-12-02</DueDate>     <ShipToName>       Yazoo Grocers - NE Distribution Center     </ShipToName>     <ShipToStreet1>12 Industrial Parkway, NW</ShipToStreet1>     <ShipToCity>Portland</ShipToCity>     <ShipToStateOrProvince>ME</ShipToStateOrProvince>     <ShipToPostalCode>04101</ShipToPostalCode>     <ItemID>HCMIN</ItemID>     <ItemQuantity>24</ItemQuantity>     <UnitPrice>2.53</UnitPrice>     <ItemDescription>       Instant Hot Cocoa Mix - Mint flavor     </ItemDescription>     <ExtendedPrice>60.72</ExtendedPrice>   </InvoiceLine> </Invoice> 

We want the result document to look like the following file.

Result (HierarchyToFlat.xml)
 <?xml version="1.0" encoding="UTF-8"?> <Invoice>   <Header>     <CustomerNumber>BQ003</CustomerNumber>     <InvoiceNumber>2002041</InvoiceNumber>     <InvoiceDate>2002-11-12</InvoiceDate>     <PONumber>AZ999345</PONumber>     <DueDate>2002-12-02</DueDate>   </Header>   <ShipTo>     <ShipToName>Yazoo Grocers - NE Distribution Center</ShipToName>     <ShipToStreet1>12 Industrial Parkway, NW</ShipToStreet1>     <ShipToStreet2/>     <ShipToCity>Portland</ShipToCity>     <ShipToStateOrProvince>ME</ShipToStateOrProvince>     <ShipToPostalCode>04101</ShipToPostalCode>     <ShipToCountry/>   </ShipTo>   <LineItemGroup>     <LineItem>       <ItemID>HCVAN</ItemID>       <ItemQuantity>12</ItemQuantity>       <UnitPrice>2.59</UnitPrice>       <ExtendedPrice>31.08</ExtendedPrice>     </LineItem>     <ItemDescription>       <Description>Instant Hot Cocoa Mix - Vanilla flavor</Description>     </ItemDescription>   </LineItemGroup>   <LineItemGroup>     <LineItem>       <ItemID>HCMIN</ItemID>       <ItemQuantity>24</ItemQuantity>       <UnitPrice>2.53</UnitPrice>       <ExtendedPrice>60.72</ExtendedPrice>     </LineItem>     <ItemDescription>       <Description>Instant Hot Cocoa Mix - Mint flavor</Description>     </ItemDescription>   </LineItemGroup> </Invoice> 

Here's the stylesheet.

Stylesheet (FlatToHierarchy.xsl)
 <?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0"     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">   <xsl:output method="xml" version="1.0" encoding="UTF-8"       indent="yes"/>   <xsl:template match="/Invoice">     <Invoice>       <xsl:call-template name="OutputHeader"/>       <xsl:call-template name="OutputShipTo"/>       <xsl:apply-templates select="InvoiceLine"/>     </Invoice>   </xsl:template>   <xsl:template name="OutputHeader">     <Header>       <CustomerNumber>         <xsl:value-of select="InvoiceLine[1]/CustomerNumber"/>       </CustomerNumber>       <InvoiceNumber>         <xsl:value-of select="InvoiceLine[1]/InvoiceNumber"/>       </InvoiceNumber>       <InvoiceDate>         <xsl:value-of select="InvoiceLine[1]/InvoiceDate"/>       </InvoiceDate>       <PONumber>         <xsl:value-of select="InvoiceLine[1]/PONumber"/>       </PONumber>       <DueDate>         <xsl:value-of select="InvoiceLine[1]/DueDate"/>       </DueDate>     </Header>   </xsl:template>   <xsl:template name="OutputShipTo">   <ShipTo>     <ShipToName>         <xsl:value-of select="InvoiceLine[1]/ShipToName"/>     </ShipToName>     <ShipToStreet1>         <xsl:value-of select="InvoiceLine[1]/ShipToStreet1"/>     </ShipToStreet1>     <ShipToStreet2>         <xsl:value-of select="InvoiceLine[1]/ShipToStreet2"/>     </ShipToStreet2>     <ShipToCity>         <xsl:value-of select="InvoiceLine[1]/ShipToCity"/>     </ShipToCity>     <ShipToStateOrProvince>         <xsl:value-of             select="InvoiceLine[1]/ShipToStateOrProvince"/>     </ShipToStateOrProvince>     <ShipToPostalCode>         <xsl:value-of select="InvoiceLine[1]/ShipToPostalCode"/>     </ShipToPostalCode>     <ShipToCountry>         <xsl:value-of select="InvoiceLine[1]/ShipToCountry"/>     </ShipToCountry>   </ShipTo>   </xsl:template>   <xsl:template match="InvoiceLine">   <LineItemGroup>     <LineItem>       <ItemID>         <xsl:value-of select="ItemID"/>       </ItemID>       <ItemQuantity>         <xsl:value-of select="ItemQuantity"/>       </ItemQuantity>       <UnitPrice>         <xsl:value-of select="UnitPrice"/>       </UnitPrice>       <ExtendedPrice>         <xsl:value-of select="ExtendedPrice"/>       </ExtendedPrice>     </LineItem>     <ItemDescription>       <Description>         <xsl:value-of select="ItemDescription"/>       </Description>     </ItemDescription>   </LineItemGroup>   </xsl:template> </xsl:stylesheet> 

There are only a few key points to understanding how this stylesheet works. You'll notice that the first xsl:template sets up the overall look of the result tree, calls two xsl:templates by name, and then invokes the final one by xsl:apply-templates. The two named xsl:templates extract values from the first InvoiceLine Element. The select expressions in the OutputHeader xsl:template use "[1]". This is an XPath predicate of the location step expression that says use the first occurrence of this node on the current axis. Since the default axis is child, and that axis moves in document order , we extract the values from the first InvoiceLine child of the current context node of Invoice. Document order is defined by the W3C as being the same order you would have if you read an XML input document serially , character by character, from a file or input stream. This may seem intuitive or overkill for some people, but specifications always have to make the obvious explicit.

The OutputShipTo xsl:template works in exactly the same way as OutputHeader. This approach in the first few xsl:template Elements lets us create and populate our two header level Elements from the contents of the first line item. The final xsl:template and the xsl:apply-templates Element that invokes it revert back to the rule-based model we are familiar with from our other examples.

I should note that because in this example we use most of the same Element names in the result tree that we do in the source, there are some other XSLT features that we could use to make this stylesheet even more compact. For simplicity's sake I'm just using features that we've covered so far.

So, that was much easier that it is in some EDI mappers I've worked with! Let's see how to go back the other way.

Mapping a Hierarchy to a Flat Structure

Although the transformation for mapping a hierarchy to a flat structure is a mirror image of what we just did, the XSLT is not. Here is how the stylesheet looks, using the same source and result files from our example above but with their roles reversed .

Stylesheet (HierarchyToFlat.xsl)
 <?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0"     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">   <xsl:output method="xml" version="1.0" encoding="UTF-8"       indent="yes"/>   <xsl:template match="/Invoice">     <Invoice>       <xsl:apply-templates select="LineItemGroup"/>     </Invoice>   </xsl:template>   <xsl:template match="LineItemGroup">   <InvoiceLine>     <CustomerNumber>       <xsl:value-of select="/Invoice/Header/CustomerNumber"/>     </CustomerNumber>     <InvoiceNumber>       <xsl:value-of select="/Invoice/Header/InvoiceNumber"/>     </InvoiceNumber>     <InvoiceDate>       <xsl:value-of select="/Invoice/Header/InvoiceDate"/>     </InvoiceDate>     <PONumber>       <xsl:value-of select="/Invoice/Header/PONumber"/>     </PONumber>     <DueDate>       <xsl:value-of select="/Invoice/Header/DueDate"/>     </DueDate>     <ShipToName>       <xsl:value-of select="/Invoice/ShipTo/ShipToName"/>     </ShipToName>     <ShipToStreet1>       <xsl:value-of select="/Invoice/ShipTo/ShipToStreet1"/>     </ShipToStreet1>     <ShipToStreet2>       <xsl:value-of select="/Invoice/ShipTo/ShipToStreet2"/>     </ShipToStreet2>     <ShipToCity>       <xsl:value-of select="/Invoice/ShipTo/ShipToCity"/>     </ShipToCity>     <ShipToStateOrProvince>       <xsl:value-of           select="/Invoice/ShipTo/ShipToStateOrProvince"/>     </ShipToStateOrProvince>     <ShipToPostalCode>       <xsl:value-of select="/Invoice/ShipTo/ShipToPostalCode"/>     </ShipToPostalCode>     <ShipToCountry>       <xsl:value-of select="/Invoice/ShipTo/ShipToCountry"/>     </ShipToCountry>     <ItemID>       <xsl:value-of select="LineItem/ItemID"/>     </ItemID>     <ItemQuantity>       <xsl:value-of select="LineItem/ItemQuantity"/>     </ItemQuantity>     <UnitPrice>       <xsl:value-of select="LineItem/UnitPrice"/>     </UnitPrice>     <ItemDescription>       <xsl:value-of select="ItemDescription/Description"/>     </ItemDescription>     <ExtendedPrice>       <xsl:value-of select="LineItem/ExtendedPrice"/>     </ExtendedPrice>   </InvoiceLine> </xsl:template> </xsl:stylesheet> 

We take the same rule-based approach in outlining the overall structure of the result tree in the first xsl:template. There isn't much to it this time. Aside from defining the document's root Element, we perform an xsl:apply-templates on the LineItemGroup. The second xsl:template, matching that select, is where the real work happens. It is very similar to what we have seen so far except in the use of absolute location paths. To repeatedly insert the contents of our Header and ShipTo Elements into each line item, we use xsl:value-of with select expressions that point to the specific relevant Elements in the source tree. For the Elements relating to the line item details and descriptions, we just use the relative path name style we have used previously.

Tips for Dealing with Other Navigation Problems

The two problems we just looked at are perhaps among the ones that most vex users of certain EDI management systems. However, these problems are by no means the only ones. Keeping the following tips in mind may make life a bit easier for you when you're dealing with such problems.

  • Start with the result! Use a rule-based approach and code your first xsl:template to reflect the overall structure of your result document. Use xsl:call-template by name if you need to set up some branches in the result tree that bear no relation to anything in the source tree. Use iterative processing with xsl:apply-templates when you do have a correspondence between source and result Elements.

  • Within an xsl:template you can use an absolute location path expression to select values from anywhere in the source tree. As with some of our examples, you can even select specific occurrences of Elements or even the Attributes of these Elements.

  • Also within a template you can use relative path location expressions to select values from up, down, sideways , up then down, or any combination of directions to get to other data related to the result node you're creating.

  • If you have several source nodes with the same subtree path expression but need to process only certain ones, you can be selective. As we saw with one of our examples, you can code select expressions to test for specific values in source tree Elements or Attributes.

The thing to remember is that, unlike the advice the droll Yankee gave to the lost tourist, "You can get there from here!"



Using XML with Legacy Business Applications
Using XML with Legacy Business Applications
ISBN: 0321154940
EAN: 2147483647
Year: 2003
Pages: 181

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net