[SalesForce] XML Parsing of Package xml to Json string

I am trying to parse a package xml and make it as json string using DOM Parser in salesforce.But I am not getting result of JSON string.Below is my code:
VF Page:

<apex:page applyHtmlTag="true" controller="Doccox">
<apex:form >
<apex:inputFile value="{!fil}" id="xmlfile"/>
<div>XML TO JSON PARSE TOOL FOR PACKAGE.XML</div>
<apex:actionFunction name="ctrlRead" action="{!parse}"/>
<apex:commandButton onclick="ctrlRead();" value="Parse" id="theButton"/>
</apex:form>
</apex:page>

Apex class:

public class Doccox{
    public Blob fil {get;set;}
    public string st {get;set;}
    public String jsonstring {get;set;}
    Map<string,string> listmap = new Map<string,string>();
    List<PackagedMembers> pm = new List<PackagedMembers>();
    public class wrapxml
    {
        public decimal version;
        public List<PackagedMembers> types;
    }
    public class PackagedMembers
    {
        public string members;
        public string name;
    }
    public void parse() {
        string st=fil.toString();
        system.debug('@@@@access_string@@'+st);
        Dom.Document doc = new Dom.Document();
        doc.load(st);
        Dom.XMLNode add =doc.getRootElement();
        system.debug('++++rootnodename+++'+add.getname());
        walkThrough(add);
    }
    public String walkThrough(DOM.XMLNode node) {
        String result = '\n';
        if (node.getNodeType() == DOM.XMLNodeType.ELEMENT) {
            for(Dom.XMLNode ch : node.getChildElements())
            {
                if(ch.getName()=='member')
                {
                    map<string,string> memstr = new Map<string,string>();
                    memstr.put(string.valueOf(ch.getParent()),ch.getText());
                    system.debug('++++memstr+++'+memstr);
                    if(ch.getName()=='name')
                    {
                        map<string,string> memstrx = new Map<string,string>();
                        memstrx.put(string.valueOf(ch.getParent()),ch.getText());
                        system.debug('++++memstrx+++'+memstrx);
                        if(memstr.keySet()==memstrx.keySet())
                        {
                            listmap.put(memstr.get(string.valueOf(ch.getParent())),memstrx.get(string.valueOf(ch.getParent())));
                            system.debug('++++listmap+++'+listmap);
                        }
                    }
                }
                result += walkThrough(ch);
            }
        }
        for(string sy :listmap.keyset())
        {
            PackagedMembers pmo = new PackagedMembers();
            pmo.members=sy;
            pmo.name=listmap.get(sy);
            pm.add(pmo);
            system.debug('++++pm+++'+pm);
        }
        wrapxml wp = new wrapxml();
        wp.version =37.0;
        wp.types=pm;
        String jsonstring = JSON.serialize(wp);
        system.debug('++++jsonstring+++'+jsonstring);
        return jsonstring;
    }
}

Best Answer

Your parsing code has a number of issues related to parsing the XML.

First, the inner members are called "members", not "member", so this line needs to be fixed:

if(ch.getName()=='members')

Next, if ch.getName() is equal to 'members', it won't be equal to 'name', because that'll be in a later pass; you can't nest your if statements like that.

Your next problem is then going to be here:

memstr.put(string.valueOf(ch.getParent()),ch.getText());

because it's just building really big, useless keys for you.

Also, you never actually assigned the value back to jsonString, so the results are getting lost.

I get where you're trying to go with this, but XML parsing is non-trivial to get right.

Also, "members" may be an array once converted, which is why it's pluralized.

I'm not sure exactly how to fix your code, but I happen to have some public code that I'll share with you: XmlToJSON.

Since we can't readily determine if we'll need the array syntax, and because my class automatically generates arrays, we can start with the automatic untyped JSON parsing:

public class Doccox{
    public Blob fil { get; set; }
    public string jsonString { get; set; }
    public void parse() {
        try {
            Dom.Document doc = new Dom.Document();
            doc.load(fil.toString());
            jsonString = XmlToJson.parseDocumentToJson(doc);
        } catch(Exception e) {
        }
    }
}

There's some additional processing you need to do if you want something other than a JSON string. To get it into a wrapper, you have to be aware that types will be either an Object or an Array, depending on if it has one or more elements. Similarly, members will either be an Object or an Array.

As a minor optimization in this case, we can alter XmlToJson to assume that elements ending with S will always be an array:

global class XmlToJson {
    public String xmlText { get; set; }
    public String getJsonText() {
        try {
            Dom.Document doc = new Dom.Document();
            doc.load(xmlText);
            return parseDocumentToJson(doc);
        } catch(Exception e) {
            return '';
        }
    }

    //  Try to determine some data types by pattern
    static Pattern 
        boolPat = Pattern.compile('^(true|false)$'),  decPat = Pattern.compile('^[-+]?\\d+(\\.\\d+)?$'), 
        datePat = Pattern.compile('^\\d{4}.\\d{2}.\\d{2}$'), 
        timePat = Pattern.compile('^\\d{4}.\\d{2}.\\d{2} (\\d{2}:\\d{2}:\\d{2} ([-+]\\d{2}:\\d{2})?)?$');
    //  Primary function to decode XML
    static Map<Object, Object> parseNode(Dom.XmlNode node, Map<Object, Object> parent) {
        //  Iterate over all child elements for a given node
        for(Dom.XmlNode child: node.getChildElements()) {
            //  Pull out some information
            String nodeText = child.getText().trim(), name = child.getName();
            //  Determine data type
            Object value = 
                //  Nothing
                String.isBlank(nodeText)? null:
            //  Try boolean
            boolPat.matcher(nodeText).find()? 
                (Object)Boolean.valueOf(nodeText):
            //  Try decimals
            decPat.matcher(nodeText).find()?
                (Object)Decimal.valueOf(nodeText):
            //  Try dates
            datePat.matcher(nodeText).find()?
                (Object)Date.valueOf(nodeText):
            //  Try times
            timePat.matcher(nodeText).find()? 
                (Object)DateTime.valueOf(nodeText):
            //  Give up, use plain text
            (Object)nodeText;
            //  We have some text to process
            if(value != null) {
                //  This is a pluralized word, make list
                if(name.endsWith('s')) {
                    // Add a new list if none exists
                    if(!parent.containsKey(name)) {
                        parent.put(name, new List<Object>());
                    }
                    // Add the value to the list
                    ((List<Object>)parent.get(name)).add(value);
                } else {
                    //  Store a new value
                    parent.put(name, value);
                }
            } else if(child.getNodeType() == Dom.XmlNodeType.ELEMENT) {
                //  If it's not a comment or text, we will recursively process the data
                Map<Object, Object> temp = parseNode(child, new Map<Object, Object>());
                //  If at least one node was processed, add a new element into the array
                if(!temp.isEmpty()) {
                    //  Again, create or update a list
                    if(parent.containsKey(name)) {
                        try {
                            //  If it's already a list, add it
                            ((List<Object>)parent.get(name)).add(temp);
                        } catch(Exception e) {
                            //  Otherwise, convert the element into a list
                            parent.put(name, new List<Object> { parent.get(name), temp });
                        }
                    } else {
                        //  New element
                        parent.put(name, temp);
                    }
                }
            }
        }
        return parent;
    }
    //  This function converts XML into a Map
    global static Map<Object, Object> parseDocumentToMap(Dom.Document doc) {
        return parseNode(doc.getRootElement(), new Map<Object, Object>());
    }
    //  This function converts XML into a JSON string
    global static String parseDocumentToJson(Dom.Document doc) {
        return JSON.serializePretty(parseDocumentToMap(doc));
    }
    //  This function converts XML into a native object
    //  If arrays are expected, but not converted automatically, this call may fail
    //  If so, use the parseDocumentToMap function instead and fix any problems
    global static Object parseDocumentToObject(Dom.Document doc, Type klass) {
        return JSON.deserialize(parseDocumentToJson(doc), klass);
    }
}

The only change here is to swap out the automatic list aggregation with automatic list assumption. This works out well for the package.xml, because the naming convention happens to work out that way.

Here's an XML file I use by default:

<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <members>*</members>
        <name>ApexClass</name>
    </types>
    <types>
        <members>*</members>
        <name>ApexComponent</name>
    </types>
    <types>
        <members>*</members>
        <name>ApexPage</name>
    </types>
    <types>
        <members>*</members>
        <name>ApexTrigger</name>
    </types>
    <types>
        <members>*</members>
        <name>StaticResource</name>
    </types>
    <version>37.0</version>
</Package>

And here's the resulting output:

{ "version" : 37.0,
  "types" : [ 
    { "name" : "ApexClass", "members" : [ "*" ] }, 
    { "name" : "ApexComponent", "members" : [ "*" ] }, 
    { "name" : "ApexPage", "members" : [ "*" ] }, 
    { "name" : "ApexTrigger", "members" : [ "*" ] }, 
    { "name" : "StaticResource", "members" : [ "*" ] }
  ]
}

There's also a corresponding unit test in the original link, above, but you'll have to modify it in order to maintain 100% coverage.