April 27, 2015

Using Javascript to Inspect & Modify JSON Payload

How to read and update JSON in OSB with JRE’s Javascript Engine. Download the full example.

In the previous article we updated a JSON payload by converting it to XML (and back).

However, the code that does the transformation is not very readable.

Can we, inside of OSB, use a scripting language which is more native to JSON?

Say, … Javascript?

See other posts about OSB & JSON:
JSON Proxies: Inspecting & Modifying the Payload (using conversion to XML)
Why JSON Does Help Direct Proxy Performance
How To Build a JSON Pass-Through Proxy in OSB
OSB and JSON Proxies: Gathering Statistics

How We Did it Last Time

To recollect our test project from the previous article: if the incoming JSON request has a “userid” field with a value, then pass the request downstream as is; otherwise, inject the value “anonymous” into the field. E.g., this request

{
  "id":1234
}

will need to be updated to become

{
  "userid":"anonymous",
  "id":1234
}

To achieve that, we converted the JSON payload into XML, and then the resulting logic looked (if we get rid of OSB specifics) as:

if( not($xmlBody/*:object/*:field[@name="userid"]) ) {
  insert
    <field xmlns="http://json.to.xml.for.osb" name="userid">
    <string value="anonymous"></string>
    </field>
  as the last element of the XML payload
}

After which we had to convert it back to JSON.

Quite a mouthful for a simple act of adding one more property to JSON!

We can expect that, when the number of modifications to the payload increases, the code becomes hard to maintain.

A Better Way is to Use a Javascript Engine

What language fits most naturally to working with JSON? It is Javascript, of course, the language where JSON was derived from.

Since Java 6, every JRE has a built-in engine for running Javascript. The following two lines will create its instance for you:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("javascript");

Now we can feed it a JSON payload, and it will interpret it as Javascript data, because JSON is Javascript.

I have made an OSB Java call out method that allows to execute arbitrary Javascript. Its signature looks like this:

/**
 * Execute a script over JSON payload and return the result.
 * @param json JSON payload as string
 * @param scriptPath Path to XML containing Javascript under OSB Projects (e.g. 'Foo/Bar/InsertCredentials.js.xml') 
 * @param xml Other parameters for Javascript
 * @return New JSON value.
 */
public static String execute(String json, String scriptPath, XmlObject xml) throws Exception {
..

As you can see, it loads the Javascript from OSB resources, identified by the script path.

UpdatingWithJS

Each script expects the payload been placed into “json” variable before the execution, and places its result (the updated payload) into the “output” variable, where the engine can pick it up.

For example, I’ve used this script to inject the “anonymous” userid into the payload:

<script>
if( !json.hasOwnProperty("userid") ) {
    json.userid = defaultAuthority;
}
output = json;
</script>

Here the ‘defaultAuthority’ variable came from the third parameter to the method, which contains Javascript variables populated by OSB.

Well! This is much cleaner and concise than our old JSON-to-XML-to-JSON implementation!

Addendum: The Complete Java Source

You can find the complete Java sources, as well as an OSB test project (Examples/JSON-Update-With-Javascript) in the attached package.

For your convenience though, here is the Java class:

package com.genericparallel;

import java.util.Map;

import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.w3c.dom.Node;

import com.bea.wli.config.Ref;
import com.bea.wli.config.component.NotFoundException;
import com.bea.wli.sb.resources.config.XmlEntryDocument;
import com.bea.wli.sb.resources.xml.XmlRepository;
import com.bea.wli.sb.resources.xquery.XqueryRepository;

public class Javascript {

    /**
     * Execute a script with the body of JSON and return the result.
     * @param json JSON data as string
     * @param scriptPath Path to XML containing Javascript under OSB Projects (e.g. 'Foo/Bar/InsertCredentials.xml') 
     * @param xml Other parameters for Javascript
     * @return New JSON value.
     */
    public static String execute(String json, String scriptPath, XmlObject xml) throws Exception {
        // load JS from path
        XmlObject scriptXml = readXml(scriptPath);

        XmlCursor cursor = scriptXml.newCursor();
        cursor.toFirstChild();
        String script = cursor.getTextValue();

        return executeInner(json,script,xml);
    }   

    private static String executeInner(String json, String script, XmlObject xml) throws Exception {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("javascript");

        // collect parameters
        Bindings bindings = engine.createBindings();
        if( xml != null ) {
            XmlObject[] params = xml.execQuery("//var");
            for (XmlObject p : params) {
                String name = null;
                String value = null;

                XmlCursor cursor = p.newCursor();
                cursor.toFirstChild();
                cursor.toFirstChild();

                String varName = cursor.getName().getLocalPart();
                if( "name".equals(varName) ) {
                    name = cursor.getTextValue();
                }
                if( "value".equals(varName) ) {
                    value = cursor.getTextValue();
                }

                cursor.toNextSibling();

                varName = cursor.getName().getLocalPart();
                if( "name".equals(varName) ) {
                    name = cursor.getTextValue();
                }
                if( "value".equals(varName) ) {
                    value = cursor.getTextValue();
                }

                if( name != null ) {
                    System.out.println("Adding: "+name+"="+value);
                    bindings.put(name, value);
                }
            }
        }

        addJSON(engine,bindings);

        // inject and parse the main value
        Object jo = engine.eval("var json = "+json+";",bindings);

        // execute the script
        engine.eval(script,bindings);

        // return the value of output variable as String
        return (String)engine.eval("JSON.stringify(output)",bindings);
    }

    private static void addJSON(ScriptEngine engine, Bindings bindings) throws ScriptException {
        engine.eval(
                "JSON = {\n"+
                "    parse: function(sJSON) { return eval('(' + sJSON + ')'); },\n"+
                "    stringify: (function () {\n"+
                "      var toString = Object.prototype.toString;\n"+
                "      var isArray = Array.isArray || function (a) { return toString.call(a) === '[object Array]'; };\n"+
                "      var escMap = {'\\\"': '\\\\\"', '\\\\': '\\\\\\\\', '\\b': '\\\\b', '\\f': '\\\\f', '\\n': '\\\\n', '\\r': '\\\\r', '\\t': '\\\\t'};\n"+
                "      var escMap = {};\n"+
                "      var escFunc = function (m) { return escMap[m] || '\\u' + (m.charCodeAt(0) + 0x10000).toString(16).substr(1); };\n"+
                "      var escRE = /[\\\"\\u0000-\\u001F\\u2028\\u2029]/g;\n"+
                "      return function stringify(value) {\n"+
                "        if (value == null) {\n"+
                "          return 'null';\n"+
                "        } else if (typeof value === 'number') {\n"+
                "          return isFinite(value) ? value.toString() : 'null';\n"+
                "        } else if (typeof value === 'boolean') {\n"+
                "          return value.toString();\n"+
                "        } else if (typeof value === 'object') {\n"+
                "          if (typeof value.toJSON === 'function') {\n"+
                "            return stringify(value.toJSON());\n"+
                "          } else if (isArray(value)) {\n"+
                "            var res = '[';\n"+
                "            for (var i = 0; i < value.length; i++)\n"+
                "              res += (i ? ', ' : '') + stringify(value[i]);\n"+
                "            return res + ']';\n"+
                "          } else if (toString.call(value) === '[object Object]') {\n"+
                "            var tmp = [];\n"+
                "            for (var k in value) {\n"+
                "              if (value.hasOwnProperty(k))\n"+
                "                tmp.push(stringify(k) + ': ' + stringify(value[k]));\n"+
                "            }\n"+
                "            return '{' + tmp.join(', ') + '}';\n"+
                "          }\n"+
                "        }\n"+
                "        return '\"' + value.toString().replace(escRE, escFunc) + '\"';\n"+
                "      };\n"+
                "    })()\n"+
                "  };",bindings);
    }

    private static XqueryRepository xqr = XqueryRepository.get();

    /**
     * Reads XML resource as XmlObject
     *  
     * @param xmlRefPath Project path e.g. "Foo/XML/Data"
     * @return XmlObject with parsed XML
     */
    private static XmlObject readXml(String xmlRefPath) {
        Ref ref = new Ref("XML", Ref.getNames(xmlRefPath));
        XmlObject xmlObject = null;
        try {
            XmlEntryDocument xmlEntryDocument = XmlRepository.get().getEntry(ref);
            String xmlContent = xmlEntryDocument.getXmlEntry().getXmlContent();
            xmlObject = XmlObject.Factory.parse(xmlContent);
        } catch (NotFoundException e) {
            throw new RuntimeException("XML Resource not found: "+xmlRefPath);
        } catch (XmlException e) {
            throw new RuntimeException("Error parsing XML content: "+xmlRefPath, e);
        }
        return xmlObject;
    }
}

Vladimir Dyuzhev, author of GenericParallel

About Me

My name is Vladimir Dyuzhev, and I'm the author of GenericParallel, an OSB proxy service for making parallel calls effortlessly and MockMotor, a powerful mock server.

I'm building SOA enterprise systems for clients large and small for almost 20 years. Most of that time I've been working with BEA (later Oracle) Weblogic platform, including OSB and other SOA systems.

Feel free to contact me if you have a SOA project to design and implement. See my profile on LinkedIn.

I live in Toronto, Ontario, Canada.  canada   Email me at info@genericparallel.com