From c90ba31b56afa516ef91044ec58af81c694a0429 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Mon, 30 Oct 2023 12:43:59 +0530 Subject: [PATCH 1/6] Add proper namespace and optional field support --- .../XMLToRecordConverter.java | 297 +++++++++++++----- .../util/ConverterUtils.java | 103 ++++-- 2 files changed, 293 insertions(+), 107 deletions(-) diff --git a/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/XMLToRecordConverter.java b/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/XMLToRecordConverter.java index 4e25440064f4..78af58d4a2a8 100644 --- a/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/XMLToRecordConverter.java +++ b/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/XMLToRecordConverter.java @@ -59,9 +59,11 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -70,6 +72,7 @@ import javax.xml.parsers.ParserConfigurationException; import static io.ballerina.xmltorecordconverter.util.ConverterUtils.escapeIdentifier; +import static io.ballerina.xmltorecordconverter.util.ConverterUtils.extractTypeDescriptorNodes; import static io.ballerina.xmltorecordconverter.util.ConverterUtils.extractUnionTypeDescNode; import static io.ballerina.xmltorecordconverter.util.ConverterUtils.getPrimitiveTypeName; import static io.ballerina.xmltorecordconverter.util.ConverterUtils.sortTypeDescriptorNodes; @@ -84,11 +87,13 @@ public class XMLToRecordConverter { private XMLToRecordConverter() {} private static final String XMLNS_PREFIX = "xmlns"; + private static final String XMLDATA = "xmldata"; + private static final String COLON = ":"; public static XMLToRecordResponse convert(String xmlValue, boolean isRecordTypeDesc, boolean isClosed, boolean forceFormatRecordFields) { Map recordToTypeDescNodes = new LinkedHashMap<>(); - Map> recordToAnnotationsNodes = new LinkedHashMap<>(); + Map recordToAnnotationNodes = new LinkedHashMap<>(); Map recordToElementNodes = new LinkedHashMap<>(); List diagnosticMessages = new ArrayList<>(); XMLToRecordResponse response = new XMLToRecordResponse(); @@ -104,7 +109,7 @@ public static XMLToRecordResponse convert(String xmlValue, boolean isRecordTypeD Document doc = docBuilder.parse(inputStream); Element rootElement = doc.getDocumentElement(); - generateRecords(rootElement, isClosed, recordToTypeDescNodes, recordToAnnotationsNodes, + generateRecords(rootElement, isClosed, recordToTypeDescNodes, recordToAnnotationNodes, recordToElementNodes, diagnosticMessages); } catch (ParserConfigurationException parserConfigurationException) { DiagnosticMessage message = DiagnosticMessage.xmlToRecordConverter100(null); @@ -127,15 +132,15 @@ public static XMLToRecordResponse convert(String xmlValue, boolean isRecordTypeD .map(entry -> { List annotations = new ArrayList<>(); String recordName = entry.getKey(); - String recordTypeName = escapeIdentifier(StringUtils.capitalize(recordName)); + String recordTypeName = getRecordName(recordName); if ((recordToTypeDescNodeEntries.indexOf(entry) == recordToTypeDescNodeEntries.size() - 1) && !recordName.equals(recordTypeName)) { String annotNameValue = recordToElementNodes.get(recordName).getLocalName(); annotations.add(getXMLNameNode(annotNameValue)); } - if (recordToAnnotationsNodes.containsKey(recordName)) { - annotations.addAll(recordToAnnotationsNodes.get(recordName)); + if (recordToAnnotationNodes.containsKey(recordName)) { + annotations.add(recordToAnnotationNodes.get(recordName)); } NodeList annotationNodes = NodeFactory.createNodeList(annotations); @@ -145,7 +150,7 @@ public static XMLToRecordResponse convert(String xmlValue, boolean isRecordTypeD Token semicolon = AbstractNodeFactory.createToken(SyntaxKind.SEMICOLON_TOKEN); return NodeFactory.createTypeDefinitionNode(metadata, null, typeKeyWord, typeName, entry.getValue(), semicolon); - }).collect(Collectors.toList()); + }).toList(); NodeList moduleMembers = AbstractNodeFactory.createNodeList(new ArrayList<>(typeDefNodes));; @@ -167,68 +172,36 @@ public static XMLToRecordResponse convert(String xmlValue, boolean isRecordTypeD private static void generateRecords(Element xmlElement, boolean isClosed, Map recordToTypeDescNodes, - Map> recordToAnnotationsNodes, + Map recordToAnnotationsNodes, Map recordToElementNodes, List diagnosticMessages) { Token recordKeyWord = AbstractNodeFactory.createToken(SyntaxKind.RECORD_KEYWORD); Token bodyStartDelimiter = AbstractNodeFactory.createToken(isClosed ? SyntaxKind.OPEN_BRACE_PIPE_TOKEN : SyntaxKind.OPEN_BRACE_TOKEN); - List recordFields = new ArrayList<>(); + List recordFields; String xmlNodeName = xmlElement.getNodeName(); if (recordToTypeDescNodes.containsKey(xmlNodeName)) { - // Process differently + RecordTypeDescriptorNode previousRecordTypeDescriptorNode = + (RecordTypeDescriptorNode) recordToTypeDescNodes.get(xmlNodeName); + List previousRecordFields = previousRecordTypeDescriptorNode.fields().stream() + .map(node -> (RecordFieldNode) node).toList(); + Map previousRecordFieldToNodes = previousRecordFields.stream() + .collect(Collectors.toMap(node -> node.fieldName().text(), Function.identity(), + (val1, val2) -> val1, LinkedHashMap::new)); + + List newRecordFields = getRecordFieldsForXMLElement(xmlElement, isClosed, recordToTypeDescNodes, + recordToAnnotationsNodes, recordToElementNodes, diagnosticMessages); + Map newRecordFieldToNodes = newRecordFields.stream() + .collect(Collectors.toMap(node -> ((RecordFieldNode) node).fieldName().text(), + node -> (RecordFieldNode) node, (e1, e2) -> e1, LinkedHashMap::new)); + + recordFields = updateRecordFields(previousRecordFieldToNodes, newRecordFieldToNodes); + } else { - org.w3c.dom.NodeList xmlNodeList = xmlElement.getChildNodes(); - for (int i = 0; i < xmlNodeList.getLength(); i++) { - org.w3c.dom.Node xmlNode = xmlNodeList.item(i); - - if (xmlNode.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) { - Element xmlElementNode = (Element) xmlNode; - boolean isLeafXMLElementNode = isLeafXMLElementNode(xmlElementNode); - if (!isLeafXMLElementNode) { - generateRecords(xmlElementNode, isClosed, recordToTypeDescNodes, recordToAnnotationsNodes, - recordToElementNodes, diagnosticMessages); - } - RecordFieldNode recordField = getRecordField(xmlElementNode, null, null, false); - if (recordFields.stream().anyMatch(recField -> ((RecordFieldNode) recField).fieldName().text() - .equals(recordField.fieldName().text()))) { - int indexOfRecordFieldNode = IntStream.range(0, recordFields.size()) - .filter(j -> ((RecordFieldNode) recordFields.get(j)).fieldName().text() - .equals(recordField.fieldName().text())).findFirst().orElse(-1); - if (indexOfRecordFieldNode == -1) { - recordFields.add(recordField); - } else { - RecordFieldNode existingRecordField = - (RecordFieldNode) recordFields.remove(indexOfRecordFieldNode); - RecordFieldNode updatedRecordField = mergeRecordFields(existingRecordField, recordField); - recordFields.add(indexOfRecordFieldNode, updatedRecordField); - } - } else { - recordFields.add(recordField); - } - } - } - org.w3c.dom.NamedNodeMap xmlAttributesMap = xmlElement.getAttributes(); - for (int i = 0; i < xmlAttributesMap.getLength(); i++) { - org.w3c.dom.Node xmlNode = xmlAttributesMap.item(i); - if (xmlNode.getNodeType() == org.w3c.dom.Node.ATTRIBUTE_NODE) { - if (xmlNode.getPrefix() != null && xmlNode.getPrefix().equals(XMLNS_PREFIX)) { - List annotationNodes = new ArrayList<>(); - if (recordToAnnotationsNodes.containsKey(xmlNodeName)) { - annotationNodes = recordToAnnotationsNodes.get(xmlNodeName); - annotationNodes.add(getXMLNamespaceNode(xmlNode.getLocalName(), xmlNode.getNodeValue())); - } else { - annotationNodes.add(getXMLNamespaceNode(xmlNode.getLocalName(), xmlNode.getNodeValue())); - } - recordToAnnotationsNodes.put(xmlNodeName, annotationNodes); - } else { - RecordFieldNode recordField = getRecordField(xmlNode); - recordFields.add(recordField); - } - } - } + recordFields = getRecordFieldsForXMLElement(xmlElement, isClosed, recordToTypeDescNodes, + recordToAnnotationsNodes, recordToElementNodes, diagnosticMessages); } NodeList fieldNodes = AbstractNodeFactory.createNodeList(recordFields); Token bodyEndDelimiter = AbstractNodeFactory.createToken(isClosed ? SyntaxKind.CLOSE_BRACE_PIPE_TOKEN : @@ -239,46 +212,178 @@ private static void generateRecords(Element xmlElement, boolean isClosed, if (!recordToTypeDescNodes.containsKey(xmlNodeName)) { recordToElementNodes.put(xmlNodeName, xmlElement); - recordToTypeDescNodes.put(xmlNodeName, recordTypeDescriptorNode); } + recordToTypeDescNodes.put(xmlNodeName, recordTypeDescriptorNode); } - private static RecordFieldNode getRecordField(Element xmlElementNode, List existingFieldNames, - Map updatedFieldNames, boolean isOptionalField) { + private static List getRecordFieldsForXMLElement(Element xmlElement, boolean isClosed, + Map recordToTypeDescNodes, + Map recordToAnnotationNodes, + Map recordToElementNodes, + List diagnosticMessages) { + List recordFields = new ArrayList<>(); + + String xmlNodeName = xmlElement.getNodeName(); + org.w3c.dom.NodeList xmlNodeList = xmlElement.getChildNodes(); + for (int i = 0; i < xmlNodeList.getLength(); i++) { + org.w3c.dom.Node xmlNode = xmlNodeList.item(i); + + if (xmlNode.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) { + Element xmlElementNode = (Element) xmlNode; + boolean isLeafXMLElementNode = isLeafXMLElementNode(xmlElementNode); + if (!isLeafXMLElementNode) { + generateRecords(xmlElementNode, isClosed, recordToTypeDescNodes, recordToAnnotationNodes, + recordToElementNodes, diagnosticMessages); + } + RecordFieldNode recordField = getRecordField(xmlElementNode, false); + if (recordFields.stream().anyMatch(recField -> ((RecordFieldNode) recField).fieldName().text() + .equals(recordField.fieldName().text()))) { + int indexOfRecordFieldNode = IntStream.range(0, recordFields.size()) + .filter(j -> ((RecordFieldNode) recordFields.get(j)).fieldName().text() + .equals(recordField.fieldName().text())).findFirst().orElse(-1); + if (indexOfRecordFieldNode == -1) { + recordFields.add(recordField); + } else { + RecordFieldNode existingRecordField = + (RecordFieldNode) recordFields.remove(indexOfRecordFieldNode); + RecordFieldNode updatedRecordField = mergeRecordFields(existingRecordField, recordField); + recordFields.add(indexOfRecordFieldNode, updatedRecordField); + } + } else { + recordFields.add(recordField); + } + } + } + org.w3c.dom.NamedNodeMap xmlAttributesMap = xmlElement.getAttributes(); + for (int i = 0; i < xmlAttributesMap.getLength(); i++) { + org.w3c.dom.Node xmlNode = xmlAttributesMap.item(i); + if (xmlNode.getNodeType() == org.w3c.dom.Node.ATTRIBUTE_NODE) { + if (xmlElement.getPrefix() != null && + xmlNode.getPrefix() != null && + xmlNode.getPrefix().equals(XMLNS_PREFIX) && + xmlElement.getPrefix().equals(xmlNode.getLocalName())) { + AnnotationNode xmlNSNode = getXMLNamespaceNode(xmlNode.getLocalName(), xmlNode.getNodeValue()); + recordToAnnotationNodes.put(xmlNodeName, xmlNSNode); + } else { + Node recordField = getRecordField(xmlNode); + recordFields.add(recordField); + } + } + } + + return recordFields; + } + + private static List updateRecordFields(Map previousRecordFieldToNodes, + Map newRecordFieldToNodes) { + List updatedRecordFields = new ArrayList<>(); + + for (Map.Entry previousRecordFieldToNode : previousRecordFieldToNodes.entrySet()) { + RecordFieldNode prevRecordFieldNode = previousRecordFieldToNode.getValue(); + RecordFieldNode newRecordFieldNode = newRecordFieldToNodes.get(previousRecordFieldToNode.getKey()); + if (newRecordFieldToNodes.containsKey(previousRecordFieldToNode.getKey())) { + TypeDescriptorNode prevTypeDescNode = (TypeDescriptorNode) prevRecordFieldNode.typeName(); + TypeDescriptorNode newTypeDescNode = (TypeDescriptorNode) newRecordFieldNode.typeName(); + MetadataNode metadataNode = prevRecordFieldNode.metadata() + .orElse(newRecordFieldNode.metadata().orElse(null)); + Token optionalToken = prevRecordFieldNode.questionMarkToken() + .orElse(newRecordFieldNode.questionMarkToken().orElse(null)); + if (prevTypeDescNode.toSourceCode().equals(newTypeDescNode.toSourceCode())) { + RecordFieldNode updatedRecordFieldNode = prevRecordFieldNode.modify() + .withMetadata(metadataNode) + .withQuestionMarkToken(optionalToken) + .apply(); + updatedRecordFields.add(updatedRecordFieldNode); + } else { + List typeDescNodesSorted = + sortTypeDescriptorNodes(extractTypeDescriptorNodes(List.of(prevTypeDescNode, + newTypeDescNode))); + TypeDescriptorNode unionTypeDescNode = createUnionTypeDescriptorNode(typeDescNodesSorted); + + RecordFieldNode updatedRecordFieldNode = prevRecordFieldNode.modify() + .withMetadata(metadataNode) + .withTypeName(unionTypeDescNode) + .withQuestionMarkToken(optionalToken) + .apply(); + updatedRecordFields.add(updatedRecordFieldNode); + } + } else { + Token questionMarkToken = AbstractNodeFactory.createToken(SyntaxKind.QUESTION_MARK_TOKEN); + RecordFieldNode updatedRecordFieldNode = prevRecordFieldNode.modify() + .withQuestionMarkToken(questionMarkToken) + .apply(); + updatedRecordFields.add(updatedRecordFieldNode); + } + } + + for (int i = 0; i < newRecordFieldToNodes.size(); i++) { + List updatedRecordFieldNames = updatedRecordFields.stream() + .map(node -> ((RecordFieldNode) node).fieldName().toSourceCode()).toList(); + String newRecordFieldName = newRecordFieldToNodes.keySet().stream().toList().get(i); + if (!updatedRecordFieldNames.contains(newRecordFieldName)) { + int insertIndex = -1; + for (String newRecordFieldToNodeKey : newRecordFieldToNodes.keySet().stream().toList() + .subList(i + 1, newRecordFieldToNodes.size())) { + insertIndex = + (previousRecordFieldToNodes.keySet().stream().toList().indexOf(newRecordFieldToNodeKey)); + if (insertIndex != -1) { + break; + } + } + Token questionMarkToken = AbstractNodeFactory.createToken(SyntaxKind.QUESTION_MARK_TOKEN); + RecordFieldNode updatedRecordFieldNode = newRecordFieldToNodes + .get(newRecordFieldToNodes.keySet().stream().toList().get(i)).modify() + .withQuestionMarkToken(questionMarkToken).apply(); + if (insertIndex != -1) { + updatedRecordFields.add(insertIndex, updatedRecordFieldNode); + } else { + updatedRecordFields.add(updatedRecordFieldNode); + } + } + } + return updatedRecordFields; + } + + private static RecordFieldNode getRecordField(Element xmlElementNode, boolean isOptionalField) { Token typeName; Token questionMarkToken = AbstractNodeFactory.createToken(SyntaxKind.QUESTION_MARK_TOKEN); - TypeDescriptorNode fieldTypeName; IdentifierToken fieldName = AbstractNodeFactory.createIdentifierToken(escapeIdentifier(xmlElementNode.getNodeName().trim())); Token optionalFieldToken = isOptionalField ? questionMarkToken : null; Token semicolonToken = AbstractNodeFactory.createToken(SyntaxKind.SEMICOLON_TOKEN); - RecordFieldNode recordFieldNode; - if (isLeafXMLElementNode(xmlElementNode)) { typeName = getPrimitiveTypeName(xmlElementNode.getFirstChild().getNodeValue()); } else { // At the moment all are considered as Objects here String elementKey = xmlElementNode.getNodeName().trim(); - String type = escapeIdentifier(StringUtils.capitalize(elementKey)); + String type = getRecordName(elementKey); typeName = AbstractNodeFactory.createIdentifierToken(type); } - fieldTypeName = NodeFactory.createBuiltinSimpleNameReferenceNode(typeName.kind(), typeName); - recordFieldNode = NodeFactory.createRecordFieldNode(null, null, - fieldTypeName, fieldName, - optionalFieldToken, semicolonToken); - return recordFieldNode; + TypeDescriptorNode fieldTypeName = NodeFactory.createBuiltinSimpleNameReferenceNode(typeName.kind(), typeName); + return NodeFactory.createRecordFieldNode(null, null, fieldTypeName, fieldName, optionalFieldToken, + semicolonToken); } - private static RecordFieldNode getRecordField(org.w3c.dom.Node xmlAttributeNode) { + private static Node getRecordField(org.w3c.dom.Node xmlAttributeNode) { Token typeName = AbstractNodeFactory.createToken(SyntaxKind.STRING_KEYWORD);; TypeDescriptorNode fieldTypeName = NodeFactory.createBuiltinSimpleNameReferenceNode(typeName.kind(), typeName); IdentifierToken fieldName = AbstractNodeFactory.createIdentifierToken(escapeIdentifier(xmlAttributeNode.getNodeName())); + Token equalToken = AbstractNodeFactory.createToken(SyntaxKind.EQUAL_TOKEN); Token semicolonToken = AbstractNodeFactory.createToken(SyntaxKind.SEMICOLON_TOKEN); NodeList annotations = AbstractNodeFactory.createNodeList(getXMLAttributeNode()); MetadataNode metadataNode = NodeFactory.createMetadataNode(null, annotations); + if (xmlAttributeNode.getPrefix() != null && + xmlAttributeNode.getPrefix().equals(XMLNS_PREFIX)) { + MinutiaeList emptyMinutiaeList = AbstractNodeFactory.createEmptyMinutiaeList(); + LiteralValueToken literalToken = NodeFactory.createLiteralValueToken(SyntaxKind.STRING_LITERAL_TOKEN, + String.format("\"%s\"", xmlAttributeNode.getTextContent()), emptyMinutiaeList, emptyMinutiaeList); + BasicLiteralNode valueExpr = NodeFactory.createBasicLiteralNode(SyntaxKind.STRING_LITERAL, literalToken); + return NodeFactory.createRecordFieldWithDefaultValueNode(metadataNode, null, fieldTypeName, + fieldName, equalToken, valueExpr, semicolonToken); + } return NodeFactory.createRecordFieldNode(metadataNode, null, fieldTypeName, fieldName, null, semicolonToken); } @@ -349,15 +454,43 @@ private static TypeDescriptorNode joinToUnionTypeDescriptorNode(List typeNames) { + if (typeNames.isEmpty()) { + Token typeName = AbstractNodeFactory.createToken(SyntaxKind.ANYDATA_KEYWORD); + return NodeFactory.createBuiltinSimpleNameReferenceNode(typeName.kind(), typeName); + } else if (typeNames.size() == 1) { + return typeNames.get(0); + } + TypeDescriptorNode unionTypeDescNode = joinToUnionTypeDescriptorNode(typeNames); + Token openParenToken = NodeFactory.createToken(SyntaxKind.OPEN_PAREN_TOKEN); + Token closeParenToken = NodeFactory.createToken(SyntaxKind.CLOSE_PAREN_TOKEN); + + return NodeFactory.createParenthesisedTypeDescriptorNode(openParenToken, unionTypeDescNode, closeParenToken); + } + + private static boolean isLeafXMLElementNode(Element xmlElement) { + return xmlElement.getChildNodes().getLength() == 1 && + xmlElement.getChildNodes().item(0).getNodeType() == org.w3c.dom.Node.TEXT_NODE; + } + + private static String getRecordName(String xmlElementNodeName) { + if (xmlElementNodeName.contains(COLON)) { + return Arrays.stream(xmlElementNodeName.split(COLON)) + .map(val -> escapeIdentifier(StringUtils.capitalize(val))).collect(Collectors.joining()); + } + return escapeIdentifier(StringUtils.capitalize(xmlElementNodeName)); } + /** + * This method returns xmldata:Name AnnotationNode with provided value. + * + * @param value String value to be passed as xmldata:Name annotation's value + * @return {@link AnnotationNode} xmldata:Name AnnotationNode + */ private static AnnotationNode getXMLNameNode(String value) { Token atToken = AbstractNodeFactory.createToken(SyntaxKind.AT_TOKEN); - IdentifierToken modulePrefix = AbstractNodeFactory.createIdentifierToken("xmldata"); + IdentifierToken modulePrefix = AbstractNodeFactory.createIdentifierToken(XMLDATA); Token colon = AbstractNodeFactory.createToken(SyntaxKind.COLON_TOKEN); IdentifierToken identifier = AbstractNodeFactory.createIdentifierToken("Name"); Node annotReference = NodeFactory.createQualifiedNameReferenceNode(modulePrefix, colon, identifier); @@ -379,10 +512,15 @@ private static AnnotationNode getXMLNameNode(String value) { return NodeFactory.createAnnotationNode(atToken, annotReference, annotValue); } + /** + * This method returns xmldata:Namespace AnnotationNode. + * + * @return {@link AnnotationNode} xmldata:Namespace AnnotationNode + */ private static AnnotationNode getXMLNamespaceNode(String prefix, String uri) { Token atToken = AbstractNodeFactory.createToken(SyntaxKind.AT_TOKEN); - IdentifierToken modulePrefix = AbstractNodeFactory.createIdentifierToken("xmldata"); + IdentifierToken modulePrefix = AbstractNodeFactory.createIdentifierToken(XMLDATA); Token colon = AbstractNodeFactory.createToken(SyntaxKind.COLON_TOKEN); IdentifierToken identifier = AbstractNodeFactory.createIdentifierToken("Namespace"); Node annotReference = NodeFactory.createQualifiedNameReferenceNode(modulePrefix, colon, identifier); @@ -419,10 +557,15 @@ private static AnnotationNode getXMLNamespaceNode(String prefix, String uri) { return NodeFactory.createAnnotationNode(atToken, annotReference, annotValue); } + /** + * This method returns xmldata:Attribute AnnotationNode. + * + * @return {@link AnnotationNode} xmldata:Attribute AnnotationNode + */ private static AnnotationNode getXMLAttributeNode() { Token atToken = AbstractNodeFactory.createToken(SyntaxKind.AT_TOKEN); - IdentifierToken modulePrefix = AbstractNodeFactory.createIdentifierToken("xmldata"); + IdentifierToken modulePrefix = AbstractNodeFactory.createIdentifierToken(XMLDATA); Token colon = AbstractNodeFactory.createToken(SyntaxKind.COLON_TOKEN); IdentifierToken identifier = AbstractNodeFactory.createIdentifierToken("Attribute"); Node annotReference = NodeFactory.createQualifiedNameReferenceNode(modulePrefix, colon, identifier); diff --git a/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/util/ConverterUtils.java b/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/util/ConverterUtils.java index 8480f7eb272b..fc665a513cd8 100644 --- a/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/util/ConverterUtils.java +++ b/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/util/ConverterUtils.java @@ -20,6 +20,7 @@ import io.ballerina.compiler.syntax.tree.AbstractNodeFactory; import io.ballerina.compiler.syntax.tree.ArrayTypeDescriptorNode; +import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.ParenthesisedTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.SyntaxInfo; import io.ballerina.compiler.syntax.tree.SyntaxKind; @@ -71,6 +72,45 @@ public static String escapeIdentifier(String identifier) { } } + /** + * This method returns the SyntaxToken corresponding to the JsonPrimitive. + * + * @param value JsonPrimitive that has to be classified. + * @return {@link Token} Classified Syntax Token. + */ + public static Token getPrimitiveTypeName(String value) { + if (isBoolean(value)) { + return AbstractNodeFactory.createToken(SyntaxKind.BOOLEAN_KEYWORD); + } else if (isInteger(value)) { + return AbstractNodeFactory.createToken(SyntaxKind.INT_KEYWORD); + } else if (isDouble(value)) { + return AbstractNodeFactory.createToken(SyntaxKind.DECIMAL_KEYWORD); + } + return AbstractNodeFactory.createToken(SyntaxKind.STRING_KEYWORD); + } + + /** + * This method extracts TypeDescriptorNodes within any UnionTypeDescriptorNodes or ParenthesisedTypeDescriptorNode. + * + * @param typeDescNodes List of Union and Parenthesised TypeDescriptorNodes + * @return {@link List} Extracted SimpleNameReferenceNodes. + */ + public static List extractTypeDescriptorNodes(List typeDescNodes) { + List extractedTypeNames = new ArrayList<>(); + for (TypeDescriptorNode typeDescNode : typeDescNodes) { + TypeDescriptorNode extractedTypeDescNode = extractParenthesisedTypeDescNode(typeDescNode); + if (extractedTypeDescNode instanceof UnionTypeDescriptorNode) { + List childTypeDescNodes = + List.of(((UnionTypeDescriptorNode) extractedTypeDescNode).leftTypeDesc(), + ((UnionTypeDescriptorNode) extractedTypeDescNode).rightTypeDesc()); + addIfNotExist(extractedTypeNames, extractTypeDescriptorNodes(childTypeDescNodes)); + } else { + addIfNotExist(extractedTypeNames, List.of(extractedTypeDescNode)); + } + } + return extractedTypeNames; + } + /** * This method returns the sorted TypeDescriptorNode list. * @@ -82,6 +122,10 @@ public static List sortTypeDescriptorNodes(List !(node instanceof ArrayTypeDescriptorNode)).collect(Collectors.toList()); List arrayNodes = typeDescriptorNodes.stream() .filter(node -> (node instanceof ArrayTypeDescriptorNode)).collect(Collectors.toList()); + List membersOfArrayNodes = arrayNodes.stream() + .map(node -> extractArrayTypeDescNode((ArrayTypeDescriptorNode) node)).toList(); + nonArrayNodes.removeIf(node -> + membersOfArrayNodes.stream().map(Node::toSourceCode).toList().contains(node.toSourceCode())); nonArrayNodes.sort(Comparator.comparing(TypeDescriptorNode::toSourceCode)); arrayNodes.sort((node1, node2) -> { ArrayTypeDescriptorNode arrayNode1 = (ArrayTypeDescriptorNode) node1; @@ -94,20 +138,6 @@ public static List sortTypeDescriptorNodes(List extractUnionTypeDescNode(TypeDescriptorNo return extractedTypeDescNodes; } + /** + * This method returns the number of dimensions of an ArrayTypeDescriptorNode. + * + * @param arrayNode ArrayTypeDescriptorNode for which the no. of dimensions has to be calculated. + * @return {@link Integer} The total no. of dimensions of the ArrayTypeDescriptorNode. + */ + public static Integer getNumberOfDimensions(ArrayTypeDescriptorNode arrayNode) { + int totalDimensions = arrayNode.dimensions().size(); + if (arrayNode.memberTypeDesc() instanceof ArrayTypeDescriptorNode) { + totalDimensions += getNumberOfDimensions((ArrayTypeDescriptorNode) arrayNode.memberTypeDesc()); + } + return totalDimensions; + } + + private static TypeDescriptorNode extractArrayTypeDescNode(ArrayTypeDescriptorNode arrayTypeDescNode) { + if (arrayTypeDescNode.memberTypeDesc() instanceof ArrayTypeDescriptorNode) { + return extractArrayTypeDescNode((ArrayTypeDescriptorNode) arrayTypeDescNode.memberTypeDesc()); + } + return arrayTypeDescNode.memberTypeDesc(); + } + private static TypeDescriptorNode extractParenthesisedTypeDescNode(TypeDescriptorNode typeDescNode) { if (typeDescNode instanceof ParenthesisedTypeDescriptorNode) { return extractParenthesisedTypeDescNode(((ParenthesisedTypeDescriptorNode) typeDescNode).typedesc()); - } else { - return typeDescNode; } + return typeDescNode; } - /** - * This method returns the SyntaxToken corresponding to the JsonPrimitive. - * - * @param value JsonPrimitive that has to be classified. - * @return {@link Token} Classified Syntax Token. - */ - public static Token getPrimitiveTypeName(String value) { - if (isBoolean(value)) { - return AbstractNodeFactory.createToken(SyntaxKind.BOOLEAN_KEYWORD); - } else if (isInteger(value)) { - return AbstractNodeFactory.createToken(SyntaxKind.INT_KEYWORD); - } else if (isDouble(value)) { - return AbstractNodeFactory.createToken(SyntaxKind.DECIMAL_KEYWORD); + private static void addIfNotExist(List typeDescNodes, + List typeDescNodesToBeInserted) { + for (TypeDescriptorNode typeDescNodeToBeInserted : typeDescNodesToBeInserted) { + if (typeDescNodes.stream().noneMatch(typeDescNode -> typeDescNode.toSourceCode() + .equals(typeDescNodeToBeInserted.toSourceCode()))) { + typeDescNodes.add(typeDescNodeToBeInserted); + } } - return AbstractNodeFactory.createToken(SyntaxKind.STRING_KEYWORD); } private static boolean isBoolean(String value) { From c9ccf892c913a59ba170874e31ef109a02aa73f6 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Mon, 30 Oct 2023 12:46:29 +0530 Subject: [PATCH 2/6] Implement test cases --- .../XMLToRecordConverterTests.java | 70 ++++++++++++++ .../test/resources/ballerina/sample_10.bal | 15 +++ .../test/resources/ballerina/sample_11.bal | 17 ++++ .../test/resources/ballerina/sample_12.bal | 18 ++++ .../test/resources/ballerina/sample_13.bal | 72 +++++++++++++++ .../test/resources/ballerina/sample_14.bal | 18 ++++ .../src/test/resources/ballerina/sample_3.bal | 6 +- .../src/test/resources/ballerina/sample_5.bal | 6 +- .../src/test/resources/ballerina/sample_6.bal | 8 +- .../src/test/resources/ballerina/sample_7.bal | 2 +- .../src/test/resources/ballerina/sample_8.bal | 10 +- .../src/test/resources/ballerina/sample_9.bal | 34 +++---- .../src/test/resources/xml/sample_10.xml | 14 +++ .../src/test/resources/xml/sample_11.xml | 23 +++++ .../src/test/resources/xml/sample_12.xml | 22 +++++ .../src/test/resources/xml/sample_13.xml | 91 +++++++++++++++++++ .../src/test/resources/xml/sample_14.xml | 7 ++ 17 files changed, 394 insertions(+), 39 deletions(-) create mode 100644 misc/xml-to-record-converter/src/test/resources/ballerina/sample_10.bal create mode 100644 misc/xml-to-record-converter/src/test/resources/ballerina/sample_11.bal create mode 100644 misc/xml-to-record-converter/src/test/resources/ballerina/sample_12.bal create mode 100644 misc/xml-to-record-converter/src/test/resources/ballerina/sample_13.bal create mode 100644 misc/xml-to-record-converter/src/test/resources/ballerina/sample_14.bal create mode 100644 misc/xml-to-record-converter/src/test/resources/xml/sample_10.xml create mode 100644 misc/xml-to-record-converter/src/test/resources/xml/sample_11.xml create mode 100644 misc/xml-to-record-converter/src/test/resources/xml/sample_12.xml create mode 100644 misc/xml-to-record-converter/src/test/resources/xml/sample_13.xml create mode 100644 misc/xml-to-record-converter/src/test/resources/xml/sample_14.xml diff --git a/misc/xml-to-record-converter/src/test/java/io/ballerina/xmltorecordconverter/XMLToRecordConverterTests.java b/misc/xml-to-record-converter/src/test/java/io/ballerina/xmltorecordconverter/XMLToRecordConverterTests.java index 7955eb8741db..817d835ee5e3 100644 --- a/misc/xml-to-record-converter/src/test/java/io/ballerina/xmltorecordconverter/XMLToRecordConverterTests.java +++ b/misc/xml-to-record-converter/src/test/java/io/ballerina/xmltorecordconverter/XMLToRecordConverterTests.java @@ -89,6 +89,31 @@ public class XMLToRecordConverterTests { private final Path sample9Bal = RES_DIR.resolve(BAL_DIR) .resolve("sample_9.bal"); + private final Path sample10XML = RES_DIR.resolve(XML_DIR) + .resolve("sample_10.xml"); + private final Path sample10Bal = RES_DIR.resolve(BAL_DIR) + .resolve("sample_10.bal"); + + private final Path sample11XML = RES_DIR.resolve(XML_DIR) + .resolve("sample_11.xml"); + private final Path sample11Bal = RES_DIR.resolve(BAL_DIR) + .resolve("sample_11.bal"); + + private final Path sample12XML = RES_DIR.resolve(XML_DIR) + .resolve("sample_12.xml"); + private final Path sample12Bal = RES_DIR.resolve(BAL_DIR) + .resolve("sample_12.bal"); + + private final Path sample13XML = RES_DIR.resolve(XML_DIR) + .resolve("sample_13.xml"); + private final Path sample13Bal = RES_DIR.resolve(BAL_DIR) + .resolve("sample_13.bal"); + + private final Path sample14XML = RES_DIR.resolve(XML_DIR) + .resolve("sample_14.xml"); + private final Path sample14Bal = RES_DIR.resolve(BAL_DIR) + .resolve("sample_14.bal"); + private static final String XMLToRecordServiceEP = "xmlToRecord/convert"; @@ -182,6 +207,51 @@ public void testComplexXMLWithNamespace() throws IOException { Assert.assertEquals(generatedCodeBlock, expectedCodeBlock); } + @Test(description = "testXMLWithOptionalFields1") + public void testXMLWithOptionalFields1() throws IOException { + String xmlFileContent = Files.readString(sample10XML); + String generatedCodeBlock = XMLToRecordConverter.convert(xmlFileContent, false, false, false) + .getCodeBlock().replaceAll("\\s+", ""); + String expectedCodeBlock = Files.readString(sample10Bal).replaceAll("\\s+", ""); + Assert.assertEquals(generatedCodeBlock, expectedCodeBlock); + } + + @Test(description = "testXMLWithOptionalFields2") + public void testXMLWithOptionalFields2() throws IOException { + String xmlFileContent = Files.readString(sample11XML); + String generatedCodeBlock = XMLToRecordConverter.convert(xmlFileContent, false, false, false) + .getCodeBlock().replaceAll("\\s+", ""); + String expectedCodeBlock = Files.readString(sample11Bal).replaceAll("\\s+", ""); + Assert.assertEquals(generatedCodeBlock, expectedCodeBlock); + } + + @Test(description = "testXMLWithOptionalFieldsInNestedNodes") + public void testXMLWithOptionalFieldsInNestedNodes() throws IOException { + String xmlFileContent = Files.readString(sample12XML); + String generatedCodeBlock = XMLToRecordConverter.convert(xmlFileContent, false, false, false) + .getCodeBlock().replaceAll("\\s+", ""); + String expectedCodeBlock = Files.readString(sample12Bal).replaceAll("\\s+", ""); + Assert.assertEquals(generatedCodeBlock, expectedCodeBlock); + } + + @Test(description = "testXMLWithOptionalFieldsInMultipleNestedNodes") + public void testXMLWithOptionalFieldsInMultipleNestedNodes() throws IOException { + String xmlFileContent = Files.readString(sample13XML); + String generatedCodeBlock = XMLToRecordConverter.convert(xmlFileContent, false, false, false) + .getCodeBlock().replaceAll("\\s+", ""); + String expectedCodeBlock = Files.readString(sample13Bal).replaceAll("\\s+", ""); + Assert.assertEquals(generatedCodeBlock, expectedCodeBlock); + } + + @Test(description = "testXMLWithMultipleNamespaces") + public void testXMLWithMultipleNamespaces() throws IOException { + String xmlFileContent = Files.readString(sample14XML); + String generatedCodeBlock = XMLToRecordConverter.convert(xmlFileContent, false, false, false) + .getCodeBlock().replaceAll("\\s+", ""); + String expectedCodeBlock = Files.readString(sample14Bal).replaceAll("\\s+", ""); + Assert.assertEquals(generatedCodeBlock, expectedCodeBlock); + } + @Test(description = "testXMLToRecordService") public void testXMLToRecordService() throws IOException, ExecutionException, InterruptedException { Endpoint serviceEndpoint = TestUtil.initializeLanguageSever(); diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_10.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_10.bal new file mode 100644 index 000000000000..49d0f283357d --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_10.bal @@ -0,0 +1,15 @@ +type Book record { + string author?; + string title; + decimal price; + (int|string) isbn; + @xmldata:Attribute + string id; +}; + +@xmldata:Name { + value: "catalog" +} +type Catalog record { + Book[] book; +}; diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_11.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_11.bal new file mode 100644 index 000000000000..bf47b839b691 --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_11.bal @@ -0,0 +1,17 @@ +type Item record { + int id; + string name; + decimal cost?; + decimal price?; + int quantity?; + string code?; + string description?; + string discount?; +}; + +@xmldata:Name { + value: "root" +} +type Root record { + Item[] item; +}; diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_12.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_12.bal new file mode 100644 index 000000000000..4b632e3b5758 --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_12.bal @@ -0,0 +1,18 @@ +type ZASN_SDTXP record { + string TDFORMAT?; + string TDLINE; + @xmldata:Attribute + string SEGMENT; +}; + +type ZASN_SDTX record { + string TDID; + string TDSPRAS; + ZASN_SDTXP[] ZASN_SDTXP; + @xmldata:Attribute + string SEGMENT; +}; + +type ROOT record { + ZASN_SDTX[] ZASN_SDTX; +}; diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_13.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_13.bal new file mode 100644 index 000000000000..d791cb6fe4b7 --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_13.bal @@ -0,0 +1,72 @@ +type Task record { + string id; + string description; + @xmldata:Attribute + string name?; +}; + +type Tasks record { + Task[] task; +}; + +type Project record { + string id; + string name; + Tasks tasks?; +}; + +type Projects record { + Project[] project; +}; + +type Manager record { + string id; + string name; +}; + +type Employee record { + int id; + string name; + string position; + int phone?; + string dob?; + Projects projects; + Manager manager?; +}; + +type Employees record { + Employee[] employee; +}; + +type Members record { + string[] member; +}; + +type Team record { + string id; + string name; + Members members; +}; + +type Teams record { + Team[] team; +}; + +type Department record { + string id; + string name; + Teams teams; +}; + +type Departments record { + Department[] department; +}; + +@xmldata:Name { + value: "company" +} +type Company record { + string name; + Employees employees; + Departments departments; +}; diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_14.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_14.bal new file mode 100644 index 000000000000..5a19fe33f959 --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_14.bal @@ -0,0 +1,18 @@ +type MainMainElement record { + string[] child\:childElement; +}; + +@xmldata:Name { + value: "root" +} +@xmldata:Namespace { +prefix: "root", + uri: "http://example.com/root" +} +type RootRoot record { + MainMainElement main\:mainElement; + @xmldata:Attribute + string xmlns\:child = "http://example.com/child"; + @xmldata:Attribute + string xmlns\:main = "http://example.com/main"; +}; diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_3.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_3.bal index aef50878136c..7b3dcf0e4b59 100644 --- a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_3.bal +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_3.bal @@ -11,10 +11,6 @@ type Codes record { @xmldata:Name { value: "bookstore" } -@xmldata:Namespace { - prefix: "ns0", - uri: "http://sample.com/test" -} type Bookstore record { string storeName; int postalCode; @@ -23,4 +19,6 @@ type Bookstore record { Codes codes; @xmldata:Attribute string status; + @xmldata:Attribute + string xmlns\:ns0 = "http://sample.com/test"; }; diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_5.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_5.bal index f874690c1016..3988e9033f45 100644 --- a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_5.bal +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_5.bal @@ -5,10 +5,6 @@ type Codes record { @xmldata:Name { value: "bookstore" } -@xmldata:Namespace { - prefix: "ns0", - uri: "http://sample.com/test" -} type Bookstore record { string storeName; int postalCode; @@ -16,4 +12,6 @@ type Bookstore record { Codes codes; @xmldata:Attribute string status; + @xmldata:Attribute + string xmlns\:ns0 = "http://sample.com/test"; }; diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_6.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_6.bal index c5caea235079..4ad5425f2ef4 100644 --- a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_6.bal +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_6.bal @@ -1,5 +1,5 @@ type Item record { - int ItemCode; + (int|string) ItemCode; }; type OtherItem record { @@ -14,10 +14,6 @@ type Codes record { @xmldata:Name { value: "bookstore" } -@xmldata:Namespace { - prefix: "ns0", - uri: "http://sample.com/test" -} type Bookstore record { string storeName; int postalCode; @@ -25,4 +21,6 @@ type Bookstore record { Codes codes; @xmldata:Attribute string status; + @xmldata:Attribute + string xmlns\:ns0 = "http://sample.com/test"; }; diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_7.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_7.bal index 77ec48994c38..afd5bda49620 100644 --- a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_7.bal +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_7.bal @@ -1,5 +1,5 @@ type Book record { - string title; + (int|string) title; string author; string genre; int published_year; diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_8.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_8.bal index ef757b282332..3f61a42bbada 100644 --- a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_8.bal +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_8.bal @@ -1,10 +1,10 @@ -type Ns0\:address record { +type Ns0Address record { string ns0\:street; string ns0\:city; string ns0\:country; }; -type Ns0\:codes record { +type Ns0Codes record { int[] ns0\:code; }; @@ -15,12 +15,12 @@ type Ns0\:codes record { prefix: "ns0", uri: "http://sample.com/test" } -type Ns0\:bookStore record { +type Ns0BookStore record { string ns0\:storeName; int ns0\:postalCode; boolean ns0\:isOpen; - Ns0\:address ns0\:address; - Ns0\:codes ns0\:codes; + Ns0Address ns0\:address; + Ns0Codes ns0\:codes; @xmldata:Attribute string status; }; diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_9.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_9.bal index fdbf9c9b8def..443813f85eea 100644 --- a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_9.bal +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_9.bal @@ -1,43 +1,37 @@ -type Ns1\:element1 record { +type Ns1Element1 record { @xmldata:Attribute string ns1\:attribute1; @xmldata:Attribute string ns2\:attribute2; }; -@xmldata:Namespace { - prefix: "xsi", - uri: "http://www.w3.org/2001/XMLSchema-instance" -} -type Ns2\:null record { +type Ns2Null record { + @xmldata:Attribute + string xmlns\:xsi = "http://www.w3.org/2001/XMLSchema-instance"; @xmldata:Attribute string xsi\:nil; }; -type Ns2\:array record { +type Ns2Array record { string[] ns2\:item; }; -type Ns2\:element2 record { +type Ns2Element2 record { string ns2\:subelement; decimal ns2\:number; boolean ns2\:boolean; - Ns2\:null ns2\:null; - Ns2\:array ns2\:array; + Ns2Null ns2\:null; + Ns2Array ns2\:array; }; @xmldata:Name { value: "root" } -@xmldata:Namespace { - prefix: "ns1", - uri: "http://namespace1.com" -} -@xmldata:Namespace { - prefix: "ns2", - uri: "http://namespace2.com" -} type Root record { - Ns1\:element1 ns1\:element1; - Ns2\:element2 ns2\:element2; + Ns1Element1 ns1\:element1; + Ns2Element2 ns2\:element2; + @xmldata:Attribute + string xmlns\:ns1 = "http://namespace1.com"; + @xmldata:Attribute + string xmlns\:ns2 = "http://namespace2.com"; }; diff --git a/misc/xml-to-record-converter/src/test/resources/xml/sample_10.xml b/misc/xml-to-record-converter/src/test/resources/xml/sample_10.xml new file mode 100644 index 000000000000..09abee3285ba --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/xml/sample_10.xml @@ -0,0 +1,14 @@ + + + + Gambardella, Matthew + XML Developer's Guide + 44.95 + 123456789 + + + Midnight Rain + 5.95 + ST1234567 + + diff --git a/misc/xml-to-record-converter/src/test/resources/xml/sample_11.xml b/misc/xml-to-record-converter/src/test/resources/xml/sample_11.xml new file mode 100644 index 000000000000..c3ca11a44424 --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/xml/sample_11.xml @@ -0,0 +1,23 @@ + + + + 1 + Item A + 19.99 + 8 + CODE1234 + + + 2 + Item B + 19.99 + 29.99 + This is a description for Item B. + + + 3 + Item C + 5 + 10% + + diff --git a/misc/xml-to-record-converter/src/test/resources/xml/sample_12.xml b/misc/xml-to-record-converter/src/test/resources/xml/sample_12.xml new file mode 100644 index 000000000000..ecbc381a94f4 --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/xml/sample_12.xml @@ -0,0 +1,22 @@ + + + + ZCA8 + E + + GS~012430880~925159POHC + + + + ZEDH + E + + / + X12-LIFNR-WE=ST~9~0124308800009 + + + / + X12-LIFNR-ST=ST~9~0124308800009~8691 JESSIE B SMITH + + + diff --git a/misc/xml-to-record-converter/src/test/resources/xml/sample_13.xml b/misc/xml-to-record-converter/src/test/resources/xml/sample_13.xml new file mode 100644 index 000000000000..0304aa7201af --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/xml/sample_13.xml @@ -0,0 +1,91 @@ + + + ABC Corporation + + + 001 + John Doe + Software Engineer + 0771234565 + + + P001 + Project A + + + T001 + Implement feature X + + + T002 + Test feature Y + + + + + P002 + Project B + + + + M001 + Manager One + + + + 002 + Jane Smith + UX Designer + 1995-Jan-01 + + + P003 + Project C + + + T005 + Create wireframes for new feature + + + + + + + + + D001 + Development + + + DT001 + Frontend Team + + John Doe + Jane Smith + + + + DT002 + Backend Team + + Another Employee + + + + + + D002 + Design + + + DT003 + UX Team + + John Doe + Jane Smith + + + + + + diff --git a/misc/xml-to-record-converter/src/test/resources/xml/sample_14.xml b/misc/xml-to-record-converter/src/test/resources/xml/sample_14.xml new file mode 100644 index 000000000000..16975e827f6d --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/xml/sample_14.xml @@ -0,0 +1,7 @@ + + + + Value 1 + Value 2 + + From b966bc1c52e7036090d22c30f37b5e729afe059b Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Tue, 31 Oct 2023 11:28:02 +0530 Subject: [PATCH 3/6] Add fix for nested nodes with repeaded same name --- .../XMLToRecordConverter.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/XMLToRecordConverter.java b/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/XMLToRecordConverter.java index 78af58d4a2a8..50033b0d9bed 100644 --- a/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/XMLToRecordConverter.java +++ b/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/XMLToRecordConverter.java @@ -53,6 +53,7 @@ import org.ballerinalang.formatter.core.FormattingOptions; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; import org.xml.sax.SAXException; import java.io.ByteArrayInputStream; @@ -179,9 +180,10 @@ private static void generateRecords(Element xmlElement, boolean isClosed, Token bodyStartDelimiter = AbstractNodeFactory.createToken(isClosed ? SyntaxKind.OPEN_BRACE_PIPE_TOKEN : SyntaxKind.OPEN_BRACE_TOKEN); - List recordFields; - String xmlNodeName = xmlElement.getNodeName(); + + List recordFields = getRecordFieldsForXMLElement(xmlElement, isClosed, recordToTypeDescNodes, + recordToAnnotationsNodes, recordToElementNodes, diagnosticMessages); if (recordToTypeDescNodes.containsKey(xmlNodeName)) { RecordTypeDescriptorNode previousRecordTypeDescriptorNode = (RecordTypeDescriptorNode) recordToTypeDescNodes.get(xmlNodeName); @@ -191,17 +193,12 @@ private static void generateRecords(Element xmlElement, boolean isClosed, .collect(Collectors.toMap(node -> node.fieldName().text(), Function.identity(), (val1, val2) -> val1, LinkedHashMap::new)); - List newRecordFields = getRecordFieldsForXMLElement(xmlElement, isClosed, recordToTypeDescNodes, - recordToAnnotationsNodes, recordToElementNodes, diagnosticMessages); - Map newRecordFieldToNodes = newRecordFields.stream() + Map newRecordFieldToNodes = recordFields.stream() .collect(Collectors.toMap(node -> ((RecordFieldNode) node).fieldName().text(), node -> (RecordFieldNode) node, (e1, e2) -> e1, LinkedHashMap::new)); + recordFields.clear(); recordFields = updateRecordFields(previousRecordFieldToNodes, newRecordFieldToNodes); - - } else { - recordFields = getRecordFieldsForXMLElement(xmlElement, isClosed, recordToTypeDescNodes, - recordToAnnotationsNodes, recordToElementNodes, diagnosticMessages); } NodeList fieldNodes = AbstractNodeFactory.createNodeList(recordFields); Token bodyEndDelimiter = AbstractNodeFactory.createToken(isClosed ? SyntaxKind.CLOSE_BRACE_PIPE_TOKEN : @@ -210,9 +207,7 @@ private static void generateRecords(Element xmlElement, boolean isClosed, NodeFactory.createRecordTypeDescriptorNode(recordKeyWord, bodyStartDelimiter, fieldNodes, null, bodyEndDelimiter); - if (!recordToTypeDescNodes.containsKey(xmlNodeName)) { - recordToElementNodes.put(xmlNodeName, xmlElement); - } + recordToElementNodes.put(xmlNodeName, xmlElement); recordToTypeDescNodes.put(xmlNodeName, recordTypeDescriptorNode); } @@ -254,7 +249,7 @@ private static List getRecordFieldsForXMLElement(Element xmlElement, boole } } } - org.w3c.dom.NamedNodeMap xmlAttributesMap = xmlElement.getAttributes(); + NamedNodeMap xmlAttributesMap = xmlElement.getAttributes(); for (int i = 0; i < xmlAttributesMap.getLength(); i++) { org.w3c.dom.Node xmlNode = xmlAttributesMap.item(i); if (xmlNode.getNodeType() == org.w3c.dom.Node.ATTRIBUTE_NODE) { @@ -274,6 +269,13 @@ private static List getRecordFieldsForXMLElement(Element xmlElement, boole return recordFields; } + /** + * This method returns a List of RecordFieldNodes that are updated if already a Record with same name exists. + * + * @param previousRecordFieldToNodes RecordFieldNodes of already existing Record of the same name + * @param newRecordFieldToNodes RecordFieldNodes of the current Record of the same name + * @return {@link List} List of updated RecordFieldNodes. + */ private static List updateRecordFields(Map previousRecordFieldToNodes, Map newRecordFieldToNodes) { List updatedRecordFields = new ArrayList<>(); From 67d0df93d7b67f97edf0923760c81505f257328e Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Tue, 31 Oct 2023 11:54:49 +0530 Subject: [PATCH 4/6] Add few more tests --- .../XMLToRecordConverterTests.java | 28 +++++++ .../test/resources/ballerina/sample_15.bal | 57 +++++++++++++ .../test/resources/ballerina/sample_16.bal | 39 +++++++++ .../src/test/resources/xml/sample_15.xml | 60 ++++++++++++++ .../src/test/resources/xml/sample_16.xml | 79 +++++++++++++++++++ 5 files changed, 263 insertions(+) create mode 100644 misc/xml-to-record-converter/src/test/resources/ballerina/sample_15.bal create mode 100644 misc/xml-to-record-converter/src/test/resources/ballerina/sample_16.bal create mode 100644 misc/xml-to-record-converter/src/test/resources/xml/sample_15.xml create mode 100644 misc/xml-to-record-converter/src/test/resources/xml/sample_16.xml diff --git a/misc/xml-to-record-converter/src/test/java/io/ballerina/xmltorecordconverter/XMLToRecordConverterTests.java b/misc/xml-to-record-converter/src/test/java/io/ballerina/xmltorecordconverter/XMLToRecordConverterTests.java index 817d835ee5e3..c4a9953f86ed 100644 --- a/misc/xml-to-record-converter/src/test/java/io/ballerina/xmltorecordconverter/XMLToRecordConverterTests.java +++ b/misc/xml-to-record-converter/src/test/java/io/ballerina/xmltorecordconverter/XMLToRecordConverterTests.java @@ -114,6 +114,16 @@ public class XMLToRecordConverterTests { private final Path sample14Bal = RES_DIR.resolve(BAL_DIR) .resolve("sample_14.bal"); + private final Path sample15XML = RES_DIR.resolve(XML_DIR) + .resolve("sample_15.xml"); + private final Path sample15Bal = RES_DIR.resolve(BAL_DIR) + .resolve("sample_15.bal"); + + private final Path sample16XML = RES_DIR.resolve(XML_DIR) + .resolve("sample_16.xml"); + private final Path sample16Bal = RES_DIR.resolve(BAL_DIR) + .resolve("sample_16.bal"); + private static final String XMLToRecordServiceEP = "xmlToRecord/convert"; @@ -252,6 +262,24 @@ public void testXMLWithMultipleNamespaces() throws IOException { Assert.assertEquals(generatedCodeBlock, expectedCodeBlock); } + @Test(description = "testComplexXMLWithMultipleNamespaces") + public void testComplexXMLWithMultipleNamespaces() throws IOException { + String xmlFileContent = Files.readString(sample15XML); + String generatedCodeBlock = XMLToRecordConverter.convert(xmlFileContent, false, false, false) + .getCodeBlock().replaceAll("\\s+", ""); + String expectedCodeBlock = Files.readString(sample15Bal).replaceAll("\\s+", ""); + Assert.assertEquals(generatedCodeBlock, expectedCodeBlock); + } + + @Test(description = "testComplexXMLWithMultipleNamespacesAndRecurringNodes") + public void testComplexXMLWithMultipleNamespacesAndRecurringNodes() throws IOException { + String xmlFileContent = Files.readString(sample16XML); + String generatedCodeBlock = XMLToRecordConverter.convert(xmlFileContent, false, false, false) + .getCodeBlock().replaceAll("\\s+", ""); + String expectedCodeBlock = Files.readString(sample16Bal).replaceAll("\\s+", ""); + Assert.assertEquals(generatedCodeBlock, expectedCodeBlock); + } + @Test(description = "testXMLToRecordService") public void testXMLToRecordService() throws IOException, ExecutionException, InterruptedException { Endpoint serviceEndpoint = TestUtil.initializeLanguageSever(); diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_15.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_15.bal new file mode 100644 index 000000000000..8687b11f0b83 --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_15.bal @@ -0,0 +1,57 @@ +type MainLocation record { + string main\:city; + string main\:state; +}; + +type MainCompanyInfo record { + string main\:name; + MainLocation main\:location; +}; + +type Project record { + string[] task?; + @xmldata:Attribute + string name; +}; + +type Manager record { + @xmldata:Attribute + string id; + @xmldata:Attribute + string name; +}; + +type Employee record { + string name; + string position?; + Project[] project; + Manager manager?; + @xmldata:Attribute + string id?; +}; + +type Employees record { + Employee[] employee; +}; + +type Mixed record { + string b; + string i; +}; + +@xmldata:Name { + value: "root" +} +@xmldata:Namespace { + prefix: "root", + uri: "http://example.com/root" +} +type RootRoot record { + MainCompanyInfo main\:companyInfo; + Employees employees; + Mixed mixed; + @xmldata:Attribute + string version; + @xmldata:Attribute + string xmlns\:main = "http://example.com/main"; +}; diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_16.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_16.bal new file mode 100644 index 000000000000..57b9affb1bc2 --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_16.bal @@ -0,0 +1,39 @@ +type CCourse record { + string u\:name; + int c\:intake?; + string p\:professor?; +}; + +type DSubDepartment record { + string d\:name; + CCourse[] c\:course?; + DSubDepartment[] d\:subDepartment?; +}; + +type DDepartment record { + string u\:name; + CCourse[] c\:course; + DSubDepartment d\:subDepartment?; +}; + +type FFaculty record { + string u\:name; + DDepartment[] d\:department; +}; + +@xmldata:Name { + value: "university" +} +type University record { + FFaculty[] f\:faculty; + @xmldata:Attribute + string xmlns\:c = "http://example.com/course"; + @xmldata:Attribute + string xmlns\:d = "http://example.com/department"; + @xmldata:Attribute + string xmlns\:f = "http://example.com/faculty"; + @xmldata:Attribute + string xmlns\:p = "http://example.com/professor"; + @xmldata:Attribute + string xmlns\:u = "http://example.com/university"; +}; \ No newline at end of file diff --git a/misc/xml-to-record-converter/src/test/resources/xml/sample_15.xml b/misc/xml-to-record-converter/src/test/resources/xml/sample_15.xml new file mode 100644 index 000000000000..d9229f75e3d0 --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/xml/sample_15.xml @@ -0,0 +1,60 @@ + + + + + + ABC Corporation + + New York + NY + + + + + + + + John Doe + Engineer + + Implement feature X + Test feature Y + + + + + + + + + + Jane Smith + UI/UX + + Create wireframes for new feature + Conduct user testing sessions + + + + + + Jane Smith + + Conduct user testing sessions + + + + + + + + + + + This is bold and italic. + + + + .]]> + + diff --git a/misc/xml-to-record-converter/src/test/resources/xml/sample_16.xml b/misc/xml-to-record-converter/src/test/resources/xml/sample_16.xml new file mode 100644 index 000000000000..20e7cdf15ccd --- /dev/null +++ b/misc/xml-to-record-converter/src/test/resources/xml/sample_16.xml @@ -0,0 +1,79 @@ + + + Engineering + + Computer Science + + Introduction to Programming + Dr. Smith + + + + Data Structures and Algorithms + Dr. Johnson + + + + ICE + + Introduction to Robotics + Dr. Chandana + + + Robotics + + Field Robotics + + Introduction to Field Robotics + + + + Haptics + + Introduction to Haptics + Dr. Moses + + + Advanced Haptics + Dr. Rajapaksha + + + + + + + Electrical Engineering + + Circuit Design + 50 + Dr. Williams + + + + + + Business Administration + + Marketing + + Consumer Behavior + Dr. Brown + + + + Strategic Marketing + Dr. Davis + + + + + Finance + + Investment Management + Dr. Miller + + + + + + From 639ed2e3f7658c420694be09ef1f5e6df11b50cc Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Mon, 13 Nov 2023 10:40:33 +0530 Subject: [PATCH 5/6] Add missing EOL char --- .../src/test/resources/ballerina/sample_16.bal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_16.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_16.bal index 57b9affb1bc2..e10e8b7248ff 100644 --- a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_16.bal +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_16.bal @@ -36,4 +36,4 @@ type University record { string xmlns\:p = "http://example.com/professor"; @xmldata:Attribute string xmlns\:u = "http://example.com/university"; -}; \ No newline at end of file +}; From 325c43b87a1ddec8134d596bc58d5dc4d210fe0e Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Mon, 13 Nov 2023 13:50:32 +0530 Subject: [PATCH 6/6] Add delimiter to diff namespace in record name --- .../XMLToRecordConverter.java | 2 +- .../test/resources/ballerina/sample_14.bal | 6 +++--- .../test/resources/ballerina/sample_15.bal | 10 +++++----- .../test/resources/ballerina/sample_16.bal | 20 +++++++++---------- .../src/test/resources/ballerina/sample_8.bal | 10 +++++----- .../src/test/resources/ballerina/sample_9.bal | 16 +++++++-------- 6 files changed, 32 insertions(+), 32 deletions(-) diff --git a/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/XMLToRecordConverter.java b/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/XMLToRecordConverter.java index 50033b0d9bed..8112eb446e55 100644 --- a/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/XMLToRecordConverter.java +++ b/misc/xml-to-record-converter/src/main/java/io/ballerina/xmltorecordconverter/XMLToRecordConverter.java @@ -478,7 +478,7 @@ private static boolean isLeafXMLElementNode(Element xmlElement) { private static String getRecordName(String xmlElementNodeName) { if (xmlElementNodeName.contains(COLON)) { return Arrays.stream(xmlElementNodeName.split(COLON)) - .map(val -> escapeIdentifier(StringUtils.capitalize(val))).collect(Collectors.joining()); + .map(val -> escapeIdentifier(StringUtils.capitalize(val))).collect(Collectors.joining("_")); } return escapeIdentifier(StringUtils.capitalize(xmlElementNodeName)); } diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_14.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_14.bal index 5a19fe33f959..f58a65e3cbaf 100644 --- a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_14.bal +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_14.bal @@ -1,4 +1,4 @@ -type MainMainElement record { +type Main_MainElement record { string[] child\:childElement; }; @@ -9,8 +9,8 @@ type MainMainElement record { prefix: "root", uri: "http://example.com/root" } -type RootRoot record { - MainMainElement main\:mainElement; +type Root_Root record { + Main_MainElement main\:mainElement; @xmldata:Attribute string xmlns\:child = "http://example.com/child"; @xmldata:Attribute diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_15.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_15.bal index 8687b11f0b83..ac6fef0cedd1 100644 --- a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_15.bal +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_15.bal @@ -1,11 +1,11 @@ -type MainLocation record { +type Main_Location record { string main\:city; string main\:state; }; -type MainCompanyInfo record { +type Main_CompanyInfo record { string main\:name; - MainLocation main\:location; + Main_Location main\:location; }; type Project record { @@ -46,8 +46,8 @@ type Mixed record { prefix: "root", uri: "http://example.com/root" } -type RootRoot record { - MainCompanyInfo main\:companyInfo; +type Root_Root record { + Main_CompanyInfo main\:companyInfo; Employees employees; Mixed mixed; @xmldata:Attribute diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_16.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_16.bal index e10e8b7248ff..44dac20e57cc 100644 --- a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_16.bal +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_16.bal @@ -1,31 +1,31 @@ -type CCourse record { +type C_Course record { string u\:name; int c\:intake?; string p\:professor?; }; -type DSubDepartment record { +type D_SubDepartment record { string d\:name; - CCourse[] c\:course?; - DSubDepartment[] d\:subDepartment?; + C_Course[] c\:course?; + D_SubDepartment[] d\:subDepartment?; }; -type DDepartment record { +type D_Department record { string u\:name; - CCourse[] c\:course; - DSubDepartment d\:subDepartment?; + C_Course[] c\:course; + D_SubDepartment d\:subDepartment?; }; -type FFaculty record { +type F_Faculty record { string u\:name; - DDepartment[] d\:department; + D_Department[] d\:department; }; @xmldata:Name { value: "university" } type University record { - FFaculty[] f\:faculty; + F_Faculty[] f\:faculty; @xmldata:Attribute string xmlns\:c = "http://example.com/course"; @xmldata:Attribute diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_8.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_8.bal index 3f61a42bbada..b7cb7d0100e1 100644 --- a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_8.bal +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_8.bal @@ -1,10 +1,10 @@ -type Ns0Address record { +type Ns0_Address record { string ns0\:street; string ns0\:city; string ns0\:country; }; -type Ns0Codes record { +type Ns0_Codes record { int[] ns0\:code; }; @@ -15,12 +15,12 @@ type Ns0Codes record { prefix: "ns0", uri: "http://sample.com/test" } -type Ns0BookStore record { +type Ns0_BookStore record { string ns0\:storeName; int ns0\:postalCode; boolean ns0\:isOpen; - Ns0Address ns0\:address; - Ns0Codes ns0\:codes; + Ns0_Address ns0\:address; + Ns0_Codes ns0\:codes; @xmldata:Attribute string status; }; diff --git a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_9.bal b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_9.bal index 443813f85eea..47f51990eaa6 100644 --- a/misc/xml-to-record-converter/src/test/resources/ballerina/sample_9.bal +++ b/misc/xml-to-record-converter/src/test/resources/ballerina/sample_9.bal @@ -1,35 +1,35 @@ -type Ns1Element1 record { +type Ns1_Element1 record { @xmldata:Attribute string ns1\:attribute1; @xmldata:Attribute string ns2\:attribute2; }; -type Ns2Null record { +type Ns2_Null record { @xmldata:Attribute string xmlns\:xsi = "http://www.w3.org/2001/XMLSchema-instance"; @xmldata:Attribute string xsi\:nil; }; -type Ns2Array record { +type Ns2_Array record { string[] ns2\:item; }; -type Ns2Element2 record { +type Ns2_Element2 record { string ns2\:subelement; decimal ns2\:number; boolean ns2\:boolean; - Ns2Null ns2\:null; - Ns2Array ns2\:array; + Ns2_Null ns2\:null; + Ns2_Array ns2\:array; }; @xmldata:Name { value: "root" } type Root record { - Ns1Element1 ns1\:element1; - Ns2Element2 ns2\:element2; + Ns1_Element1 ns1\:element1; + Ns2_Element2 ns2\:element2; @xmldata:Attribute string xmlns\:ns1 = "http://namespace1.com"; @xmldata:Attribute