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"andvalue.factexists but is not already an array → wrap it:{ fact: [value.fact] } - If
key === "deviceManagementResponse"andvalue.deviceDataexists 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;
}
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;
}
}
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;
}
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);
And in parseAndUnwrapSoapResponse(), after domToJson() returns, add:
JsonNode envelope = applyCustomReplacer(domToJson(doc.getDocumentElement()));
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
});
}
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 + ">";
}
}
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));
});
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)