Can we convert object into XML inside Javascipt

I"m working in a scenario where I'm getting xml object from the httpClient's getResponse().content. And I need to deal with values that are present in the response. But its not allowing me to extract those values in javascript.

Note: I cannot use XML-to-JSON like policies in my use-case; I can only work with javascript. Because I'm calling multiple HTTP requests based on the ids present in the input.

var url = properties.endpoint + '?billingId=' + billingIds[index];
var myRequest = new Request(url, "GET");

context.setVariable("myRequesttest", myRequest);
var exchange = httpClient.send(myRequest);
exchange.waitForComplete();

if (exchange.isSuccess()) {
    var responseObj = exchange.getResponse().content;
    context.setVariable("response.content", responseObj);
    context.setVariable("myResponse", responseObj);
} else if (exchange.isError()) {
  context.setVariable("myResponseError", exchange.getError());
}

help?

Solved Solved
1 7 996
1 ACCEPTED SOLUTION

You can use the JS callout to invoke multiple external systems (via the builtin httpClient), and you can then use logic in the JS to combine the results into one object or payload. This is nice if the number of external systems you need to invoke is not determined until runtime. Maybe it's dependent upon the data provided in the input request, or based on data returned by the first call. So this is handy.

Having said that, there are limitations. The JS callout has a timeout on it, and if execution time exceeds that timeout, then the JS policy will fail. If the responses from your remote systems do not all return before the JS policy timeout expires, you won't get the results. Also, in a JS callout you cannot use the various npm modules that help you out with parsing XML and so on. For a more general solution then, I'd suggest a dedicated app logic host - something like Google App Engine, Cloud Run, or similar. But you can do it with a JS callout if you are comfortable with the limitations.

OK with the caveats and warnings out of the way, this is what worked for me.

// invokeTransformAndCombine.js

var baseUrl = 'https://my-base-url.com/foobar';
var ids = [182872, 19882, 2133];
var accumulator = {};

// the asynchronous response handler
function onComplete(id) {
  return function(response, error) {
    if (response) {
      if (response.status == 200) {
        var xmlobj = new XML(response.content);
        var json = E4XtoJSON(xmlobj);
        accumulator[id] = json;
        if (Object.keys(accumulator).length == ids.length) {
          context.setVariable('combined', JSON.stringify(accumulator, null, 2));
        }
      }
    }
    else {
      context.setVariable('requesterror_' + id, 'Whoops: ' + error);
    }
  };
}

// send out one request
function invokeOne(id) {
  var url = baseUrl + '?id=' + id ;
  var headers = {
        Authorization : 'optional, pass whatever you like',
        'ID-Index' : id
      };
  var req = new Request(url, 'GET', headers);
  httpClient.send(req, onComplete(id));
}

// set the default value (empty object)
context.setVariable('combined', JSON.stringify(accumulator, null, 2));

// action starts here
ids.forEach(invokeOne);

Let me explain the code.

  1. First, the real action starts at the line with ids.forEach(). This forEach invokes a function one time for each of the ID's in the array.
  2. The invokeOne function uses the httpClient.send() method. This form is a little different than what you were using. It employs the callback function as the 2nd argument. The way this one works: httpClient.send() sends out the request, and ... returns. When the http runtime receives the response to that request, it invokes the callback function to handle the response.
  3. onComplete() is not a callback function. It is a function that returns a callback function, wrapped in a closure. The reason we want this: that way the callback has access to the ID that the response corresponds to. The inner function (within the closure) is the actual callback function.
  4. The inner callback - there is some magic going on there. It checks the status code and only in the case of success (200 code) it does a few things.

        var xmlobj = new XML(response.content);
        var json = E4XtoJSON(xmlobj);
        accumulator[id] = json;
        if (Object.keys(accumulator).length == ids.length) {
          context.setVariable('combined', JSON.stringify(accumulator, null, 2));
        }
    	
    1. First, the JS callout in Apigee supports E4X, which is the moniker for Ecmascript-for-XML. That extension to the Ecmascript language has since been deprecated in the main line of Ecmascript as driven by the TC39 Tech committee. But it still works in Apigee. So this line of code converts the text XML response into an XML 'object', which can then be handled in JS.
    2. Then, it invokes the mystical function E4XtoJSON. This is defined elsewhere, but as you can infer from the name, this function converts the XML object to JSON.
    3. it drops that json as a value in the accumulator object.
    4. and when enough responses have been received, it stringifies that accumulator into a context variable.

All of that should be pretty clear. The remaining mystery is the E4XtoJSON function. I got that from this gist. To use it within a JS inside Apigee, I had to drop that code into a separate 'resource' under the jsc directory in the proxy bundle, and then configure the policy this way:

<Javascript name='JS-InvokeTransformAndCombine' timeLimit='2200' >
  <IncludeURL>jsc://E4XtoJSON.js</IncludeURL>
  <ResourceURL>jsc://invokeTransformAndCombine.js</ResourceURL>
</Javascript>

When I invoke this repeatedly with a service that returns XML shaped like this:

<billinfo>
  <stamp>1613696215531</stamp>
  <localtime>2021-02-19T00:56:55Z</localtime>
  <id>902</id>
  <status>rejected</status>
  <code>JN23H</code>
  <amount>376.44</amount>
</billinfo>

... the output of the Javascript I showed above is a combined JSON blob that looks like this:

{
  "19882": {
    "stamp": "1613696265138",
    "localtime": "2021-02-19T00:57:45Z",
    "id": "19882",
    "status": "ready",
    "code": "E8237k",
    "amount": "788.10"
  },
  "182872": {
    "stamp": "1613696265136",
    "localtime": "2021-02-19T00:57:45Z",
    "id": "182872",
    "status": "split",
    "code": "JN23H",
    "amount": "280.94"
  },
  "2133": {
    "stamp": "1613696265141",
    "localtime": "2021-02-19T00:57:45Z",
    "id": "2133",
    "status": "rejected",
    "code": "SIWU97",
    "amount": "847.50"
  }
}

If you don't want the full combined thing, but rather just a few fields extracted from the XML response, you can take the same approach but just grab the fields you want.

I haven't tried the E4XtoJSON with XML that uses namespaces. That's a wrinkle you will have to sort out.

helpful?

View solution in original post

7 REPLIES 7

You can use the JS callout to invoke multiple external systems (via the builtin httpClient), and you can then use logic in the JS to combine the results into one object or payload. This is nice if the number of external systems you need to invoke is not determined until runtime. Maybe it's dependent upon the data provided in the input request, or based on data returned by the first call. So this is handy.

Having said that, there are limitations. The JS callout has a timeout on it, and if execution time exceeds that timeout, then the JS policy will fail. If the responses from your remote systems do not all return before the JS policy timeout expires, you won't get the results. Also, in a JS callout you cannot use the various npm modules that help you out with parsing XML and so on. For a more general solution then, I'd suggest a dedicated app logic host - something like Google App Engine, Cloud Run, or similar. But you can do it with a JS callout if you are comfortable with the limitations.

OK with the caveats and warnings out of the way, this is what worked for me.

// invokeTransformAndCombine.js

var baseUrl = 'https://my-base-url.com/foobar';
var ids = [182872, 19882, 2133];
var accumulator = {};

// the asynchronous response handler
function onComplete(id) {
  return function(response, error) {
    if (response) {
      if (response.status == 200) {
        var xmlobj = new XML(response.content);
        var json = E4XtoJSON(xmlobj);
        accumulator[id] = json;
        if (Object.keys(accumulator).length == ids.length) {
          context.setVariable('combined', JSON.stringify(accumulator, null, 2));
        }
      }
    }
    else {
      context.setVariable('requesterror_' + id, 'Whoops: ' + error);
    }
  };
}

// send out one request
function invokeOne(id) {
  var url = baseUrl + '?id=' + id ;
  var headers = {
        Authorization : 'optional, pass whatever you like',
        'ID-Index' : id
      };
  var req = new Request(url, 'GET', headers);
  httpClient.send(req, onComplete(id));
}

// set the default value (empty object)
context.setVariable('combined', JSON.stringify(accumulator, null, 2));

// action starts here
ids.forEach(invokeOne);

Let me explain the code.

  1. First, the real action starts at the line with ids.forEach(). This forEach invokes a function one time for each of the ID's in the array.
  2. The invokeOne function uses the httpClient.send() method. This form is a little different than what you were using. It employs the callback function as the 2nd argument. The way this one works: httpClient.send() sends out the request, and ... returns. When the http runtime receives the response to that request, it invokes the callback function to handle the response.
  3. onComplete() is not a callback function. It is a function that returns a callback function, wrapped in a closure. The reason we want this: that way the callback has access to the ID that the response corresponds to. The inner function (within the closure) is the actual callback function.
  4. The inner callback - there is some magic going on there. It checks the status code and only in the case of success (200 code) it does a few things.

        var xmlobj = new XML(response.content);
        var json = E4XtoJSON(xmlobj);
        accumulator[id] = json;
        if (Object.keys(accumulator).length == ids.length) {
          context.setVariable('combined', JSON.stringify(accumulator, null, 2));
        }
    	
    1. First, the JS callout in Apigee supports E4X, which is the moniker for Ecmascript-for-XML. That extension to the Ecmascript language has since been deprecated in the main line of Ecmascript as driven by the TC39 Tech committee. But it still works in Apigee. So this line of code converts the text XML response into an XML 'object', which can then be handled in JS.
    2. Then, it invokes the mystical function E4XtoJSON. This is defined elsewhere, but as you can infer from the name, this function converts the XML object to JSON.
    3. it drops that json as a value in the accumulator object.
    4. and when enough responses have been received, it stringifies that accumulator into a context variable.

All of that should be pretty clear. The remaining mystery is the E4XtoJSON function. I got that from this gist. To use it within a JS inside Apigee, I had to drop that code into a separate 'resource' under the jsc directory in the proxy bundle, and then configure the policy this way:

<Javascript name='JS-InvokeTransformAndCombine' timeLimit='2200' >
  <IncludeURL>jsc://E4XtoJSON.js</IncludeURL>
  <ResourceURL>jsc://invokeTransformAndCombine.js</ResourceURL>
</Javascript>

When I invoke this repeatedly with a service that returns XML shaped like this:

<billinfo>
  <stamp>1613696215531</stamp>
  <localtime>2021-02-19T00:56:55Z</localtime>
  <id>902</id>
  <status>rejected</status>
  <code>JN23H</code>
  <amount>376.44</amount>
</billinfo>

... the output of the Javascript I showed above is a combined JSON blob that looks like this:

{
  "19882": {
    "stamp": "1613696265138",
    "localtime": "2021-02-19T00:57:45Z",
    "id": "19882",
    "status": "ready",
    "code": "E8237k",
    "amount": "788.10"
  },
  "182872": {
    "stamp": "1613696265136",
    "localtime": "2021-02-19T00:57:45Z",
    "id": "182872",
    "status": "split",
    "code": "JN23H",
    "amount": "280.94"
  },
  "2133": {
    "stamp": "1613696265141",
    "localtime": "2021-02-19T00:57:45Z",
    "id": "2133",
    "status": "rejected",
    "code": "SIWU97",
    "amount": "847.50"
  }
}

If you don't want the full combined thing, but rather just a few fields extracted from the XML response, you can take the same approach but just grab the fields you want.

I haven't tried the E4XtoJSON with XML that uses namespaces. That's a wrinkle you will have to sort out.

helpful?

Hi Dino, thank you so much for your response, but I'm facing a problem while using this javascript.
I'm getting this error:

Execution of JS-PrepareCheckOutagesResponse failed with error: Javascript runtime error: "ReferenceError: "require" is not defined. (E4XtoJSON.js:54)"

You need to remove line 54 and below from the gist. Those lines are only for testing.

Got it, thanks again.

Hi Dino, in my javascript asynchronous code is not working. I tried to print data inside the callback function but didn't get any data in the console and after callback function, every statement is getting executed.

Hi Pawan,

The print statements won't show up where you might hope. But the asynch callback is working.

The problem is happening in collection of trace information; the execution of the callback code successfully happens, even if you don't see your print statements where you think they'd be. There is an outstanding bug on this, ref: b/111777025.

I suggest that you rely not on print statements, but rather, on the values of context variables to determine the outcome of the policy as you develop it and test . Use AssignMessage to inspect the variables of interest in a step that follows the JS callout. Here's a tip for how to do that.

okay, thanks for your response, I'll try by using AssignMessage policy.