Created
August 2, 2024 07:48
-
-
Save scolladon/34e4c1574a071914abd687a7770fb6db to your computer and use it in GitHub Desktop.
apex iterative XML parser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
}*/ | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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