DEV Community

Charlie Fubon
Charlie Fubon

Posted on

Gauai

Good, now I have the full picture. Here's what needs to be ported from utils.js:


parseAttribute() — what it actually does

It's not a generic attribute parser. It does one very specific thing: finds deviceIdentifier entries in request.deviceRequest.deviceIdentifier[] where mobileDevice is present, and injects an $: { "xsi:type": "rsa_csd:MobileDevice" } attribute onto that element so xml2js will serialize it with the correct xsi:type XML attribute.

In Java this means before calling jsonNodeToXml(), you need to walk payload.request.deviceRequest.deviceIdentifier array, find any entry with a mobileDevice child, and inject "$": { "xsi:type": "rsa_csd:MobileDevice" } onto it. Your jsonNodeToXml() will also need to handle the $ key specially — treat it as XML attributes on the parent element rather than child elements.


addNamespace() — what it actually does

Recursive walk of the entire JSON object, prefixing every key with namespace + ":" (i.e. rsa_csd:) unless skipNamespace is true. skipNamespace is set to true when the key is "$" (the attributes bag from parseAttribute) — those must NOT get the namespace prefix since they're XML attributes, not elements.

So { messageHeader: { requestType: "ANALYZE" } } becomes { "rsa_csd:messageHeader": { "rsa_csd:requestType": "ANALYZE" } }.

This runs on the whole payload before XML serialization, which is why inner elements get rsa_csd: prefixes in the SOAP envelope.


customReplacer() — what it actually does

Two specific array-normalization rules applied during JSON serialization:

  • If key === "clientFactList" and value.fact exists but is not already an array → wrap it: { fact: [value.fact] }
  • If key === "deviceManagementResponse" and value.deviceData exists but is not already an array → wrap it: value.deviceData = [value.deviceData]

This handles RSA returning a single object where the schema allows multiple — without it, downstream code that expects an array will break.


Port plan — add to RSAAdapter.java directly

parseAttribute — add a private method:

private JsonNode parseAttribute(JsonNode payload) {
    // Only acts on request.deviceRequest.deviceIdentifier[]
    JsonNode deviceIdentifiers = payload.path("request")
            .path("deviceRequest").path("deviceIdentifier");
    if (deviceIdentifiers.isMissingNode() || !deviceIdentifiers.isArray()) return payload;

    ObjectNode mutablePayload = payload.deepCopy();
    com.fasterxml.jackson.databind.node.ArrayNode identifiers =
            (com.fasterxml.jackson.databind.node.ArrayNode)
            mutablePayload.path("request").path("deviceRequest").path("deviceIdentifier");

    for (int i = 0; i < identifiers.size(); i++) {
        ObjectNode entry = (ObjectNode) identifiers.get(i);
        if (!entry.path("mobileDevice").isMissingNode()) {
            ObjectNode mobileDevice = (ObjectNode) entry.get("mobileDevice");
            // inject $ attributes bag: { "xsi:type": "rsa_csd:MobileDevice" }
            ObjectNode attrs = objectMapper.createObjectNode();
            attrs.put("xsi:type", "rsa_csd:MobileDevice");
            mobileDevice.set("$", attrs);
            entry.set("mobileDevice", mobileDevice);
            identifiers.set(i, entry);
        }
    }
    return mutablePayload;
}
Enter fullscreen mode Exit fullscreen mode

addNamespace — add a private method:

private JsonNode addNamespace(JsonNode node, String namespace, boolean skipNamespace) {
    if (node.isArray()) {
        com.fasterxml.jackson.databind.node.ArrayNode newArr =
                objectMapper.createArrayNode();
        node.forEach(item -> newArr.add(addNamespace(item, namespace, skipNamespace)));
        return newArr;
    } else if (node.isObject()) {
        ObjectNode newObj = objectMapper.createObjectNode();
        node.fields().forEachRemaining(entry -> {
            String key = entry.getKey();
            JsonNode val = entry.getValue();
            boolean skip = skipNamespace || key.equals("$");
            JsonNode transformed = addNamespace(val, namespace, skip);
            if (skip) {
                newObj.set(key, transformed);
            } else {
                newObj.set(namespace + ":" + key, transformed);
            }
        });
        return newObj;
    } else {
        // primitive — return as-is
        return node;
    }
}
Enter fullscreen mode Exit fullscreen mode

customReplacer — apply during domToJson() response parsing, not during request building. Add a post-processing step after domToJson() returns:

private JsonNode applyCustomReplacer(JsonNode node) {
    if (node.isObject()) {
        ObjectNode result = objectMapper.createObjectNode();
        node.fields().forEachRemaining(entry -> {
            String key = entry.getKey();
            JsonNode val = entry.getValue();
            if (key.equals("clientFactList") && val.isObject()
                    && val.has("fact") && !val.get("fact").isArray()) {
                ObjectNode fixed = objectMapper.createObjectNode();
                fixed.set("fact", objectMapper.createArrayNode().add(val.get("fact")));
                result.set(key, fixed);
            } else if (key.equals("deviceManagementResponse") && val.isObject()
                    && val.has("deviceData") && !val.get("deviceData").isArray()) {
                ObjectNode fixed = val.deepCopy();
                ((ObjectNode) fixed).set("deviceData",
                        objectMapper.createArrayNode().add(val.get("deviceData")));
                result.set(key, fixed);
            } else {
                result.set(key, applyCustomReplacer(val));
            }
        });
        return result;
    } else if (node.isArray()) {
        com.fasterxml.jackson.databind.node.ArrayNode arr = objectMapper.createArrayNode();
        node.forEach(item -> arr.add(applyCustomReplacer(item)));
        return arr;
    }
    return node;
}
Enter fullscreen mode Exit fullscreen mode

Wire them into doVendorCall()

Replace the current Step 5 comment block with:

// Step 5: parseAttribute + addNamespace (utils.js — now ported)
payload = parseAttribute(payload);
payload = addNamespace(payload, "rsa_csd", false);
String xmlBody = buildSoapEnvelope(payload, callType);
Enter fullscreen mode Exit fullscreen mode

And in parseAndUnwrapSoapResponse(), after domToJson() returns, add:

JsonNode envelope = applyCustomReplacer(domToJson(doc.getDocumentElement()));
Enter fullscreen mode Exit fullscreen mode

Also: jsonNodeToXml() needs to handle the $ attributes bag

Currently it ignores $. Add special handling:

private String jsonNodeToXml(JsonNode node) {
    if (node == null || node.isNull() || node.isMissingNode()) return "";
    StringBuilder sb = new StringBuilder();
    node.fields().forEachRemaining(entry -> {
        String key = entry.getKey();
        JsonNode val = entry.getValue();
        if (key.equals("$")) return; // handled as attributes on parent — skip here

        // Build attribute string from sibling "$" node if present
        String attrs = "";
        if (node.has("$")) {
            StringBuilder attrSb = new StringBuilder();
            node.get("$").fields().forEachRemaining(a ->
                attrSb.append(" ").append(a.getKey())
                       .append("=\"").append(a.getValue().asText()).append("\""));
            attrs = attrSb.toString();
        }
        // ... rest of existing logic, add attrs to opening tag
    });
}
Enter fullscreen mode Exit fullscreen mode

Wait — the current loop structure won't work cleanly for this since attrs would repeat for every sibling field. Restructure the $ attribute injection so it's applied at the caller level, not inside the loop. Specifically, jsonNodeToXml should check if the node it's about to serialize as a child has a $ sibling and inject those as XML attributes. The cleanest fix is to pass the attribute string into a helper:

private String serializeElement(String tag, JsonNode val, String xmlAttrs) {
    if (val.isObject()) {
        return "<" + tag + xmlAttrs + ">" + jsonNodeToXml(val) + "</" + tag + ">";
    } else if (val.isArray()) {
        StringBuilder sb = new StringBuilder();
        val.forEach(item -> sb.append("<").append(tag).append(xmlAttrs).append(">")
                .append(jsonNodeToXml(item)).append("</").append(tag).append(">"));
        return sb.toString();
    } else {
        return "<" + tag + xmlAttrs + ">" + val.asText() + "</" + tag + ">";
    }
}
Enter fullscreen mode Exit fullscreen mode

Then in jsonNodeToXml():

node.fields().forEachRemaining(entry -> {
    String key = entry.getKey();
    if (key.equals("$")) return; // skip — handled as attributes
    JsonNode val = entry.getValue();
    String xmlAttrs = "";
    if (val.isObject() && val.has("$")) {
        StringBuilder attrSb = new StringBuilder();
        val.get("$").fields().forEachRemaining(a ->
            attrSb.append(" ").append(a.getKey())
                   .append("=\"").append(a.getValue().asText()).append("\""));
        xmlAttrs = attrSb.toString();
    }
    sb.append(serializeElement(key, val, xmlAttrs));
});
Enter fullscreen mode Exit fullscreen mode

After these changes, buildSoapEnvelope will produce correctly namespace-prefixed XML with proper xsi:type attributes on mobileDevice elements, which is what RSA actually expects. That clears the last structural blocker on the request side.

Top comments (1)

Collapse
 
gchar profile image
Charlie Fubon

private JsonNode postRawXml(String url, ObjectNode headers, String xmlBody) throws Exception {
    org.apache.hc.client5.http.classic.methods.HttpPost post =
            new org.apache.hc.client5.http.classic.methods.HttpPost(url);
    headers.fields().forEachRemaining(h -> post.setHeader(h.getKey(), h.getValue().asText()));
    post.setEntity(new org.apache.hc.core5.http.io.entity.StringEntity(
            xmlBody, org.apache.hc.core5.http.ContentType.TEXT_XML));

    // reuse the inherited client field from APITask
    try (var response = this.client.execute(post)) {
        ObjectNode root = objectMapper.createObjectNode();
        root.put("status", String.valueOf(response.getCode()));
        root.put("message", response.getReasonPhrase());
        String body = org.apache.hc.core5.http.io.entity.EntityUtils.toString(response.getEntity());
        root.put("data", body != null ? body : "");
        return root;
    }
}

Enter fullscreen mode Exit fullscreen mode