Friday, March 22, 2013

A better way to generate XML on Salesforce using VisualForce

There are easier ways to generate XML on Salesforce than either the Dom library or XmlStreamWriter class.  If you've done either, perhaps you'll recognize the code below.

public static void DomExample()
{
    Dom.Document doc = new Dom.Document();
    
    Dom.Xmlnode rootNode = doc.createRootElement('response', null, null);

    list accountList = [ 
        select  id, name, 
                (select id, name, email from Contacts) 
          from  Account 
    ];
          
    for (Account eachAccount : accountList) {
        Dom.Xmlnode accountNode = rootNode.addChildElement('Account', null, null);
        accountNode.setAttribute('id', eachAccount.Id);
        accountNode.setAttribute('name', eachAccount.Name);
        
        for (Contact eachContact : eachAccount.Contacts) {
            Dom.Xmlnode contactNode = accountNode.addChildElement('Contact', null, null);
            contactNode.setAttribute('id', eachContact.Id);ac
            contactNode.setAttribute('name', eachContact.Name);
            contactNode.setAttribute('email', eachContact.Email);
        }
    }
    
    system.debug(doc.toXmlString());            
}

Or maybe this example.

public static void StreamExample()
{
    XmlStreamWriter writer = new XmlStreamWriter();
    
    writer.writeStartDocument('utf-8', '1.0');        
    writer.writeStartElement(null, 'response', null);
    
    list accountList = [ 
        select  id, name, 

                (select id, name, email from Contacts) 
          from  Account 
    ];
          
    for (Account eachAccount : accountList) {
        writer.writeStartElement(null, 'Account', null);
        writer.writeAttribute(null, null, 'id', eachAccount.Id);
        writer.writeAttribute(null, null, 'name', eachAccount.Name);        

        for (Contact eachContact : eachAccount.Contacts) {
            writer.writeStartElement(null, 'Contact', null);
            
            writer.writeAttribute(null, null, 'id', eachContact.Id);
            writer.writeAttribute(null, null, 'name', eachContact.Name);
            writer.writeAttribute(null, null, 'email', eachContact.Email);
            
            writer.writeEndElement();
        }
        
        writer.writeEndElement();
    }
    
    writer.writeEndElement();
    
    system.debug(writer.getXmlString());
    
    writer.close();            
}

But wouldn't you rather write something like this?

public static void PageExample()
{
    PageReference aPage = Page.AccountContactsXML;
    aPage.setRedirect(true);
    system.debug(aPage.getContent().toString());
}

Let's take a look at what makes creating the XML possible with so few lines of Apex.

Rather than build our XML using Apex code, we can type it directly into a Visualforce page--providing we strip all VF's page accessories off using apex:page attributes.


<apex:page StandardController="Account" recordSetVar="Accounts" contentType="text/xml" showHeader="false" sidebar="false" cache="false">
<?xml version="1.0" encoding="UTF-8" ?>
<response>
<apex:repeat value="{!Accounts}" var="eachAccount" >
    <Account id="{!eachAccount.id}" name="{!eachAccount.name}">&
    <apex:repeat value="{!eachAccount.contacts}" var="eachContact">
        <Contact id="{!eachContact.id}" name="{!eachContact.name}" email="{!eachContact.email}"/>
    </apex:repeat>
    </Account>
</apex:repeat>
</response>
</apex:page>

The secret that makes this code work is setting the page's API version 19.0 inside its metadata.  That is the only thing that allows the <?xml ?> processing instruction to appear at the top without the Visualforce compiler throwing Conniptions (a subclass of Exception). 

Depending on how much XML you need to generate, another advantage to the VisualForce version is how few script statements are required to produce it.

Number of code statements: 4 out of 200000

Our Dom and Stream examples require 28 and 37 respectively--and that's in a developer org with only three accounts and three contacts.  Additionally, the Page example is only 18 lines including both the .page and .cls, whereas the Dom and Stream examples are 27 and 38 lines respectively (coincidence?).

But what happens when we add billing and shipping addresses (and two more contacts)?

Our page example's Apex code doesn't change, but its page does.

<apex:page StandardController="Account" recordSetVar="Accounts" contentType="text/xml" showHeader="false" sidebar="false" cache="false">
<?xml version="1.0" encoding="UTF-8" ?>
<response>
<apex:repeat value="{!Accounts}" var="eachAccount" >    
    <Account id="{!eachAccount.id}" name="{!eachAccount.name}">
        <apex:outputPanel rendered="{!!IsBlank(eachAccount.billingStreet)}" layout="none">
            <Address type="Billing">
                <Street>{!eachAccount.billingStreet}</Street>
                <City>{!eachAccount.billingCity}</City>
                <State>{!eachAccount.billingState}</State>
                <PostalCode>{!eachAccount.billingPostalCode}</PostalCode>
                <Country>{!eachAccount.billingCountry}</Country>
            </Address>        
        </apex:outputPanel>        
        <apex:outputPanel rendered="{!!IsBlank(eachAccount.shippingStreet)}" layout="none">            
            <Address type="Shipping">
                <Street>{!eachAccount.shippingStreet}</Street>
                <City>{!eachAccount.shippingCity}</City>
                <State>{!eachAccount.shippingState}</State>
                <PostalCode>{!eachAccount.shippingPostalCode}</PostalCode>
                <Country>{!eachAccount.shippingCountry}</Country>
            </Address>
        </apex:outputPanel>
        <apex:repeat value="{!eachAccount.contacts}" var="eachContact">&
            <Contact id="{!eachContact.id}" name="{!eachContact.name}" email="{!eachContact.email}"/>
        </apex:repeat>
    </Account>
</apex:repeat>
</response>
</apex:page>

We've added sections for both the billing and shipping codes, with conditional rendering in-case either doesn't exist.  In addition to our six lines of Apex (PageExample() above) we've added 12 new lines to the earlier 18 for a total of 36 lines.  The best part is, even with the extra XML being generated our Page example will still only consume 4 script statements of the already-insufficient 200,000.

How do our Dom and Stream examples fair?  Both are pasted together below into a single code section.

public static void DomExample()
{
    Dom.Document doc = new Dom.Document();        
    
    Dom.Xmlnode rootNode = doc.createRootElement('response', null, null);

    list accountList = [ 
        select    id, name, 
                billingStreet, billingCity,
                billingState, billingPostalCode,
                billingCountry,
                shippingStreet, shippingCity,
                shippingState, shippingPostalCode,
                shippingCountry,
                (select id, name, email from Contacts) 
          from    Account ];
          
    for (Account eachAccount : accountList) {
        Dom.Xmlnode accountNode = rootNode.addChildElement('Account', null, null);
        accountNode.setAttribute('id', eachAccount.Id);
        accountNode.setAttribute('name', eachAccount.Name);
        
        if (String.IsNotBlank(eachAccount.billingStreet)) {
            Dom.Xmlnode addressNode = accountNode.addChildElement('Address', null, null);
            addressNode.setAttribute('type', 'Billing');
            addressNode.addChildElement('Street', null, null).addTextNode(eachAccount.billingStreet);
            addressNode.addChildElement('City', null, null).addTextNode(eachAccount.billingCity);
            addressNode.addChildElement('State', null, null).addTextNode(eachAccount.billingState);
            addressNode.addChildElement('PostalCode', null, null).addTextNode(eachAccount.billingPostalCode);
            addressNode.addChildElement('Country', null, null).addTextNode(eachAccount.billingCountry);                
        }
        
        if (String.IsNotBlank(eachAccount.ShippingStreet)) {                
            Dom.Xmlnode addressNode = accountNode.addChildElement('Address', null, null);
            addressNode.setAttribute('type', 'Shipping');
            addressNode.addChildElement('Street', null, null).addTextNode(eachAccount.shippingStreet);
            addressNode.addChildElement('City', null, null).addTextNode(eachAccount.shippingCity);
            addressNode.addChildElement('State', null, null).addTextNode(eachAccount.shippingState);
            addressNode.addChildElement('PostalCode', null, null).addTextNode(eachAccount.shippingPostalCode);
            addressNode.addChildElement('Country', null, null).addTextNode(eachAccount.shippingCountry);                
        }
        
        for (Contact eachContact : eachAccount.Contacts) {
            Dom.Xmlnode contactNode = accountNode.addChildElement('Contact', null, null);
            contactNode.setAttribute('id', eachContact.Id);
            contactNode.setAttribute('name', eachContact.Name);
            contactNode.setAttribute('email', eachContact.Email);
        }
    }
    
    system.debug(doc.toXmlString());            
}

public static void StreamExample()
{
    XmlStreamWriter writer = new XmlStreamWriter();
    
    writer.writeStartDocument('utf-8', '1.0');        
    writer.writeStartElement(null, 'response', null);
    
    list accountList = [ 
        select    id, name, 
                billingStreet, billingCity,
                billingState, billingPostalCode,
                billingCountry,
                shippingStreet, shippingCity,
                shippingState, shippingPostalCode,
                shippingCountry,
                (select id, name, email from Contacts) 
          from    Account ];
          
    for (Account eachAccount : accountList) {
        writer.writeStartElement(null, 'Account', null);
        writer.writeAttribute(null, null, 'id', eachAccount.Id);
        writer.writeAttribute(null, null, 'name', eachAccount.Name);
        
        if (String.IsNotBlank(eachAccount.billingStreet)) {
            writer.writeStartElement(null, 'Address', null);
            writer.writeAttribute(null, null, 'type', 'Billing');                
            
            writer.writeStartElement(null, 'Street', null);
            writer.writeCharacters(eachAccount.billingStreet);
            writer.writeEndElement();
            
            writer.writeStartElement(null, 'City', null);
            writer.writeCharacters(eachAccount.billingCity);
            writer.writeEndElement();
            
            writer.writeStartElement(null, 'State', null);
            writer.writeCharacters(eachAccount.billingState);
            writer.writeEndElement();
            
            writer.writeStartElement(null, 'PostalCode', null);
            writer.writeCharacters(eachAccount.billingPostalCode);
            writer.writeEndElement();
            
            writer.writeStartElement(null, 'Country', null);
            writer.writeCharacters(eachAccount.billingCountry);
            writer.writeEndElement();

            writer.writeEndElement();                
        }
        
        if (String.IsNotBlank(eachAccount.shippingStreet)) {
            writer.writeStartElement(null, 'Address', null);
            writer.writeAttribute(null, null, 'type', 'Shipping');                
            
            writer.writeStartElement(null, 'Street', null);
            writer.writeCharacters(eachAccount.shippingStreet);
            writer.writeEndElement();
            
            writer.writeStartElement(null, 'City', null);
            writer.writeCharacters(eachAccount.shippingCity);
            writer.writeEndElement();
            
            writer.writeStartElement(null, 'State', null);
            writer.writeCharacters(eachAccount.shippingState);
            writer.writeEndElement();
            
            writer.writeStartElement(null, 'PostalCode', null);
            writer.writeCharacters(eachAccount.shippingPostalCode);
            writer.writeEndElement();
            
            writer.writeStartElement(null, 'Country', null);
            writer.writeCharacters(eachAccount.shippingCountry);
            writer.writeEndElement();

            writer.writeEndElement();                
        }

        for (Contact eachContact : eachAccount.Contacts) {
            writer.writeStartElement(null, 'Contact', null);
            
            writer.writeAttribute(null, null, 'id', eachContact.Id);
            writer.writeAttribute(null, null, 'name', eachContact.Name);
            writer.writeAttribute(null, null, 'email', eachContact.Email);
            
            writer.writeEndElement();
        }
        
        writer.writeEndElement();
    }
    
    writer.writeEndElement();
    
    system.debug(writer.getXmlString());
    
    writer.close();            
}

Our Dom example is 52 lines and takes 60 script statements and our Stream example has ballooned to 96 lines and takes 104 script statements on our tiny data set.  For anyone keeping track, PageExample() has 30% fewer lines than DomExample() and 63% fewer lines than StreamExample().  Most importantly, no matter how much data is involved, PageExample will only ever use 4 script statements while the other two will scale gometrically as each new row of data requires more than one script statement to generate.

Caveats and disclaimers

  • The page above is about as basic as I could come up.  It stands alone and requires no controllers.  Readers should be able to paste it directly into their development orgs and see what they get (dont' forget to set the API to 19.0).
  • So basic a page doesn't take into account ordering of the data.  If the XML data needs to be in a specific order a controller would be required to return that list back to the page using a SOQL "order by" clause.
  • Though this technique is great for generating XML it can't consume XML.  That's probably obvious to programmers but is important to point-out for management types that may visit.
  • XSL stylesheets can be easily reference from the XML page by simply adding <?xml-stylesheet type="text/xsl" href="..."?> after the <?xml ?> instruction.  Such a thing can be done with PageExample() and StreamExample(), but the Dom classes don't allow adding processing instructions that I know of.
  • It's impossible to use getContent() inside test methods. 

Note: This article was originally published March 22, 2013 at it.toolbox.com on Anything Worth Doing.

6 comments:

  1. Hi Thomas,

    Greetings of the day!

    Thanks for your revert!

    I did not understand, about "What affect does it have when you try "10" or "0"?"

    I have used "apex:repeat's row attribute"

    I wanted to share the code with you to have a look at it, however, it is not allowing me here to post it.

    Any thing which suggest on why it is not displaying details of all the client records. there are more than 4000 client records in the database.

    your help would be highly appreciated.

    Thanks
    Nitesh




    nitesh78 12 days ago
    Hi Thomas,


    I have used the given code for generating a report in XML, with a help of VF page in Salesforce Enterprise version org for a custom object.

    However, it is displaying just four to five records in XML, however the object contains, thousand of records.

    I have tried many times, and many ways to sort this out, however it displays the same result.

    I would appreciate, if you can help me with this.


    Thanks
    Nitesh


    Thomas Gagne 14 minutes ago
    Nitesh have you tried the apex:repeat's row attribute? What affect does it have when you try "10" or "0"?

    Also, you might want to post on the new site where I moved my blog.

    http://doingpoorly.blogspot.com/2013/03/a-better-way-to-generate-xml-on.html

    ReplyDelete
  2. Hi Thomas,

    Also, when the XML report is generated it is displaying

    one liner with each client record
    span id="j_id0:j_id3:0:j_id5"

    above is displayed under tags span
    I do not understand why it is showing this line, I do not want it, in my report.

    May this helps.

    Thanks
    Nitesh

    ReplyDelete
  3. Is there a form on your XML? Seeing the code would be helpful. I have a gmail account. My username is tggagne+doingpoorly.

    ReplyDelete
  4. Hi Tom,

    Greetings!

    I have sent the details on your Google Id.

    It would be great help, if you can comment on it.

    Thanks
    Nitesh

    ReplyDelete
    Replies
    1. Nitesh, I haven't received the email yet. Did you add @gmail to it?

      Delete
  5. I'm worried this approach may not work after API v. 19.0 is retired. If someone has tried using this approach with a more recent API version, please let me know.

    https://help.salesforce.com/s/articleView?id=000351312&type=1

    ReplyDelete

Follow @TomGagne