Thursday, 26 June 2014

Parse zipped XML files on visualforce page

Salesforce doesn't have direct method to read files. We can do it either by external service written in Java/.Net or using javascript.

We will be discussing to parse data coming in zipped XML over http from 3-party API and populate data in salesforce. Basically, there were three main tasks:

  1. Collect http reponse from API in form of zipped xml.
  2. Unzip that response and get xml string out of it.
  3. Parse xml string to insert data.
Main focus of this blog is to accomplish Task 2 but first let see the key points for tasks 1 & 3:

Task 1: 
  • You just need to call response.getBodyAsBlob() when ever response is in form of file.
  • Then, you need to convert bolb into string using code syntax: 
String xmlzippedFile = EncodingUtil.base64Encode(res.getBodyAsBlob());

Task 3:
  • If your XML string contains <![CDATA[value]]> tag then make use of XmlStreamReader class.
  • If not, then you can either use Document class or XmlStreamReader class.

Task 2:


Main task to unzip xml file, the steps to achieve are mentioned below:
  • Add jZip.js file to static resource (download)
  • Add inflate.js file to static resource. (download)
  • Open notepad, paste below code and save as file.js file and upload to static resource.
var filearr = [];

/* Main unzip function */
function unzip(zip){
 model.getEntries(zip, function(entries) {
  entries.forEach(function(entry) {
   model.getEntryFile(entry, "Blob");
  });
 });
}

//model for zip.js
var model = (function() {
 var URL = window.webkitURL || window.mozURL || window.URL;
 var acount = 0;
 var bcount = 0;

 //compile a list of file extensions and content types
 var mapping = {
   "pdf":"application/pdf",
   "zip":"application/zip",
   "rar":"application/rar",
   "json":"application/json",
   "mid":"audio/mid",
   "mp3":"audio/mpeg",
   "bmp":"image/bmp",
   "gif":"image/gif",
   "png":"image/png",
   "jpg":"image/jpeg",
   "jpeg":"image/jpeg",
   "svg":"image/svg+xml",
   "xml":"text/xml"
  }
 return {
  getEntries : function(file, onend) {
   zip.createReader(new zip.BlobReader(file), function(zipReader) {
    zipReader.getEntries(onend);
   }, onerror);
  },
  getEntryFile : function(entry, creationMethod, onend, onprogress) {
   acount++;
   var writer, zipFileEntry;
   function getData() {
    entry.getData(writer, function(blob) {
     bcount++;
     filearr.push(blob);
     if(acount == bcount){
      //your vf page method
      waitForProcess();
     }
    }, onprogress);
   }
   var extension = entry.filename.substring(entry.filename.indexOf(".")+1);
   var mime = mapping[extension] || 'text/plain';
   writer = new zip.BlobWriter(mime);
   getData();
  }
 };
 })();
  • Create a VF page and include reference to jZip.js and file.js
  • Create a button on VF page to invoke a javascript remoting method which returns a string from Task 1. (Or you can also place a input file element with button if you just want to upload zipped from client machine). 
Below is VF page code:
<!-- place after page start-->

    <meta charset="utf8" />       
     <apex:includeScript value="{!URLFOR($Resource.jQuery, '')}"/>
    <apex:includeScript value="{!URLFOR($Resource.zip, '')}"/>
    
    <script>
    
    $( document ).ready(function() {
        //Call loading
        requestFile();            
    });
    
    function base64toBlob(base64Data, contentType) {
        contentType = contentType || '';
        var sliceSize = 1024;
        var byteCharacters = atob(base64Data);
        var bytesLength = byteCharacters.length;
        var slicesCount = Math.ceil(bytesLength / sliceSize);
        var byteArrays = new Array(slicesCount);
    
        for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
            var begin = sliceIndex * sliceSize;
            var end = Math.min(begin + sliceSize, bytesLength);
    
            var bytes = new Array(end - begin);
            for (var offset = begin, i = 0 ; offset < end; ++i, ++offset) {
                bytes[i] = byteCharacters[offset].charCodeAt(0);
            }
            byteArrays[sliceIndex] = new Uint8Array(bytes);
        }
        return new Blob(byteArrays, { type: contentType });
    }
    
    function requestFile() {
            var param = 5; //set your param to pass in method
            Visualforce.remoting.Manager.invokeAction(
            '{!$RemoteAction.APEX_PAGE_CONTROLLER_NAME(GLOBAL).methodName}',
            param,
            function(result, event){
                if (event.status) {
                    //call unzip method
                    unzip(base64toBlob(result, ''));
                } else if (event.type === 'exception') {
                    console.log(event.message);
                }
            },  
            {escape: true}
        );
    }
    
    var xmlStrResult = [];
    var isExecuted = false;
    var timerVal;
    
       //your vf page method

function waitForProcess(){
        console.log('call timer');
        timerVal = setInterval('prepareXMLString()', 10);        
    }
    
    function prepareXMLString() {
        if(!isExecuted){            
            $.each(filearr, function(index, file){
                var reader = new FileReader();  // Create a FileReader object
                
                reader.onload = function() {    // Define an event handler
                    try {
                        var xmlDoc = $.parseXML(reader.result);
                        var xmlString = (new XMLSerializer()).serializeToString(xmlDoc);
                        xmlStrResult.push(xmlString); // Display file contents
                    }catch(err) {
                        //TODO
                    }       
                }
                reader.readAsText(file);
                isExecuted = true;
            });
        }
        
        if(xmlStrResult.length != 0){
            clearInterval(timerVal);
            $('#showData').val(xmlStrResult[0]);
            console.log('Done');          
        }
    }
    </script>
    
    <apex:pageBlock >
        <apex:pageBlockSection columns="1" title="Your XML string">
            <apex:pageBlockSectionItem >
               <textarea id="showData" name="showData" rows="30" cols="150">
               Please wait....
               </textarea>
            </apex:pageBlockSectionItem>
        </apex:pageBlockSection>
    </apex:pageBlock>
    
    <apex:includeScript value="{!$Resource.file}" loadOnReady="true"/>


<!-- place before page ends-->

Key points to remember: 
  • Remoting is required, when you get file from apex controller as http response or from attachment/static resource.
  • In case of input file element, pass file directly to unzip() method.
Happy coding :) !!