Skip to content

Instantly share code, notes, and snippets.

@scolladon
Created August 2, 2024 07:48
Show Gist options
  • Save scolladon/34e4c1574a071914abd687a7770fb6db to your computer and use it in GitHub Desktop.
Save scolladon/34e4c1574a071914abd687a7770fb6db to your computer and use it in GitHub Desktop.
apex iterative XML parser
public class XMLParser {
public Object parse(Dom.XMLNode root) {
return new XMLNodeProcessor(root).process();
}
private class XMLNodeProcessor {
private final Dom.XMLNode root;
private final Map<Dom.XMLNode, Object> resultMap;
private final Map<Dom.XMLNode, Map<String, Object>> pendingNodes;
private final Map<Dom.XMLNode, Map<String, Integer>> nodeNameCountMap;
private final List<Dom.XMLNode> processingStack;
public XMLNodeProcessor(Dom.XMLNode root) {
this.root = root;
this.resultMap = new Map<Dom.XMLNode, Object>();
this.pendingNodes = new Map<Dom.XMLNode, Map<String, Object>>();
this.nodeNameCountMap = new Map<Dom.XMLNode, Map<String, Integer>>();
this.processingStack = new List<Dom.XMLNode>();
}
public Object process() {
preprocess();
processNodes();
return resultMap.get(root);
}
private void preprocess() {
List<Dom.XMLNode> preprocessStack = new List<Dom.XMLNode>();
preprocessStack.add(root);
while (!preprocessStack.isEmpty()) {
Dom.XMLNode current = preprocessStack.remove(preprocessStack.size() - 1);
initializeNodeMaps(current);
addChildrenToPreprocessStack(current, preprocessStack);
processingStack.add(current);
}
}
private void initializeNodeMaps(Dom.XMLNode node) {
pendingNodes.put(node, new Map<String, Object>());
nodeNameCountMap.put(node, new Map<String, Integer>());
}
private void addChildrenToPreprocessStack(Dom.XMLNode parent, List<Dom.XMLNode> preprocessStack) {
for (Dom.XMLNode child : parent.getChildElements()) {
updateNodeNameCount(parent, child);
preprocessStack.add(child);
}
}
private void updateNodeNameCount(Dom.XMLNode parent, Dom.XMLNode child) {
String nodeName = child.getName();
Map<String, Integer> nodeNameCount = nodeNameCountMap.get(parent);
Integer count = nodeNameCount.get(nodeName) ?? 0;
nodeNameCount.put(nodeName, count + 1);
}
private void processNodes() {
while (!processingStack.isEmpty()) {
Dom.XMLNode current = processingStack.remove(processingStack.size() - 1);
processNode(current);
}
}
private void processNode(Dom.XMLNode node) {
List<Dom.XMLNode> children = node.getChildElements();
if (children.isEmpty()) {
resultMap.put(node, node.getText());
return;
}
Map<String, Object> jsonResult = pendingNodes.get(node);
Map<String, Integer> nodeNameCount = nodeNameCountMap.get(node);
for (Dom.XMLNode child : children) {
processChildNode(child, jsonResult, nodeNameCount);
}
resultMap.put(node, jsonResult);
}
private void processChildNode(Dom.XMLNode child, Map<String, Object> jsonResult, Map<String, Integer> nodeNameCount) {
String nodeName = child.getName();
switch on child.getNodeType() {
when ELEMENT {
processElementNode(child, nodeName, jsonResult, nodeNameCount);
}
/*when TEXT {
processTextNode(child, nodeName, jsonResult);
}*/
}
}
private void processElementNode(Dom.XMLNode child, String nodeName, Map<String, Object> jsonResult, Map<String, Integer> nodeNameCount) {
Object temp = resultMap.get(child);
Boolean childIsArray = (nodeNameCount.get(nodeName) > 1);
if (childIsArray) {
addToArrayResult(nodeName, temp, jsonResult);
} else {
jsonResult.put(nodeName, temp);
}
}
private void addToArrayResult(String nodeName, Object value, Map<String, Object> jsonResult) {
if (!jsonResult.containsKey(nodeName)) {
jsonResult.put(nodeName, new List<Object>{ value });
} else {
((List<Object>) jsonResult.get(nodeName)).add(value);
}
}
/*private void processTextNode(
Dom.XMLNode child,
String nodeName,
Map<String, Object> jsonResult
) {
String textValue = child.getText();
if (String.isNotBlank(textValue)) {
jsonResult.put(nodeName, textValue.trim());
}
}*/
}
}
@isTest
private class XMLParserTest {
@isTest
static void givenSimpleXML_whenParsed_returnsExpectedResult() {
// Arrange
// Création d'un document XML simple pour tester le parseur
String xmlString = '<root><child>Hello</child><child>World</child></root>';
Dom.Document doc = new Dom.Document();
doc.load(xmlString);
Dom.XMLNode rootNode = doc.getRootElement();
XMLParser sut = new XMLParser(); // System Under Test
// Act
// Exécution de la méthode parse
Object result = sut.parse(rootNode);
// Assert
// Vérification des résultats
Map<String, Object> expected = new Map<String, Object>{
'child' => new List<String>{ 'Hello', 'World' }
};
Assert.areEqual(
expected,
result,
'Le résultat ne correspond pas à l\'attendu pour le XML simple.'
);
}
@isTest
static void givenComplexXML_whenParsed_returnsExpectedResult() {
// Arrange
// Création d'un document XML plus complexe pour tester le parseur
String xmlString = '<root><parent><child>Hello</child><child>World</child></parent></root>';
Dom.Document doc = new Dom.Document();
doc.load(xmlString);
Dom.XMLNode rootNode = doc.getRootElement();
XMLParser sut = new XMLParser(); // System Under Test
// Act
// Exécution de la méthode parse
Object result = sut.parse(rootNode);
// Assert
// Vérification des résultats
Map<String, Object> expected = new Map<String, Object>{
'parent' => new Map<String, Object>{
'child' => new List<String>{ 'Hello', 'World' }
}
};
Assert.areEqual(
expected,
result,
'Le résultat ne correspond pas à l\'attendu pour le XML complexe.'
);
}
@isTest
static void givenEmptyXML_whenParsed_returnsEmptyResult() {
// Arrange
// Création d'un document XML vide pour tester le parseur
String xmlString = '<root/>';
Dom.Document doc = new Dom.Document();
doc.load(xmlString);
Dom.XMLNode rootNode = doc.getRootElement();
XMLParser sut = new XMLParser(); // System Under Test
// Act
// Exécution de la méthode parse
Object result = sut.parse(rootNode);
// Assert
// Vérification des résultats
Assert.areEqual(
'',
result,
'Le résultat ne correspond pas à l\'attendu pour le XML vide.'
);
}
@isTest
static void givenXMLWithTextNodes_whenParsed_returnsExpectedResult() {
// Arrange
// Création d'un document XML avec des nœuds de texte pour tester le parseur
String xmlString = '<root>Hello <child>World</child>!</root>';
Dom.Document doc = new Dom.Document();
doc.load(xmlString);
Dom.XMLNode rootNode = doc.getRootElement();
XMLParser sut = new XMLParser(); // System Under Test
// Act
// Exécution de la méthode parse
Object result = sut.parse(rootNode);
// Assert
// Vérification des résultats
Map<String, Object> expected = new Map<String, Object>{
'child' => 'World'
};
Assert.areEqual(
expected,
result,
'Le résultat ne correspond pas à l\'attendu pour le XML avec des nœuds de texte.'
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment