diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index afd9674..f948987 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "data.xmldata" -version = "0.1.1" +version = "0.1.2" authors = ["Ballerina"] keywords = ["xml"] repository = "https://github.com/ballerina-platform/module-ballerina-data-xmldata" @@ -12,5 +12,5 @@ export = ["data.xmldata"] [[platform.java17.dependency]] groupId = "io.ballerina.lib" artifactId = "data-native" -version = "0.1.1" -path = "../native/build/libs/data.xmldata-native-0.1.1.jar" +version = "0.1.2" +path = "../native/build/libs/data.xmldata-native-0.1.2-SNAPSHOT.jar" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index 2a0250b..0ee6f03 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,4 +3,4 @@ id = "constraint-compiler-plugin" class = "io.ballerina.lib.data.xmldata.compiler.XmldataCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/data.xmldata-compiler-plugin-0.1.1.jar" +path = "../compiler-plugin/build/libs/data.xmldata-compiler-plugin-0.1.2-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 57bd1b1..bcfbdef 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -10,7 +10,7 @@ distribution-version = "2201.9.0" [[package]] org = "ballerina" name = "data.xmldata" -version = "0.1.1" +version = "0.1.2" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina/tests/toXml_test.bal b/ballerina/tests/toXml_test.bal index 0950703..e48ee94 100644 --- a/ballerina/tests/toXml_test.bal +++ b/ballerina/tests/toXml_test.bal @@ -1165,6 +1165,171 @@ isolated function testRecordWithNamespaceAnnotationToXml1() returns error? { test:assertEquals(result.toString(), expected, msg = "testComplexRecordToXml result incorrect"); } +type AddressR1 record {| + string city; + int code; +|}; + +type Wsa_ReplyTo1 record { + @Namespace {prefix: "wsa", uri: "example1.com"} + AddressR1 Address; +}; + +type Htng_ReplyTo1 record { + @Namespace {prefix: "wsa", uri: "example1.com"} + AddressR1 Address; +}; + +@Name {value: "soap"} +type Soap1 record { + @Name {value: "ReplyTo"} + @Namespace {prefix: "wsa", uri: "example1.com"} + Wsa_ReplyTo1 wsaReplyTo; + @Name {value: "ReplyTo"} + @Namespace {prefix: "htng", uri: "example2.com"} + Htng_ReplyTo1 htngReplyTo; +}; + +@test:Config { + groups: ["toXml", "testFail"] +} +isolated function testXmlToRecordWithNamespaceAttachedToFields() returns error? { + Soap1 val = { + htngReplyTo: { + Address: { + code: 40000, + city: "Colombo" + } + }, + wsaReplyTo: { + Address: { + code: 10000, + city: "Kandy" + } + } + }; + + xml xmlVal = check toXml(val); + string expected = "" + + "" + + "Kandy10000" + + "" + + "Colombo40000" + + ""; + test:assertEquals(xmlVal.toString(), expected); +} + +@Namespace {prefix: "wsa", uri: "example1.com"} +type AddressR2 record {| + string city; + int code; +|}; + +@Namespace {prefix: "wsa", uri: "example1.com"} +type Wsa_ReplyTo2 record { + @Namespace {prefix: "wsa", uri: "example1.com"} + AddressR2 Address; +}; + +@Namespace {prefix: "htng", uri: "example2.com"} +type Htng_ReplyTo2 record { + @Namespace {prefix: "wsa", uri: "example1.com"} + AddressR2 Address; +}; + +@Name {value: "soap"} +type Soap2 record { + @Name {value: "ReplyTo"} + @Namespace {prefix: "wsa", uri: "example1.com"} + Wsa_ReplyTo2 wsaReplyTo; + @Name {value: "ReplyTo"} + @Namespace {prefix: "htng", uri: "example2.com"} + Htng_ReplyTo2 htngReplyTo; +}; + +@test:Config { + groups: ["toXml"] +} +isolated function testXmlToRecordWithNamespaceAttachedToFieldsAndTypes() returns error? { + Soap2 val = { + htngReplyTo: { + Address: { + code: 40000, + city: "Colombo" + } + }, + wsaReplyTo: { + Address: { + code: 10000, + city: "Kandy" + } + } + }; + + xml xmlVal = check toXml(val); + string expected = "" + + "" + + "Kandy10000" + + "" + + "Colombo40000" + + ""; + test:assertEquals(xmlVal.toString(), expected); +} + +type RequestorID record { + @Attribute + string ID; + @Attribute + string ID_Context; + @Attribute + string Type; +}; + +type Source record { + RequestorID RequestorID; +}; + +@test:Config { + groups: ["toXml"] +} +isolated function testUnderscoreInTheFieldName() returns error? { + Source s = { + RequestorID: { + ID: "1", + ID_Context: "2", + Type: "3"}}; + xml xmlVal = check toXml(s); + test:assertEquals(xmlVal.toString(), ""); +} + +@Namespace { + uri: "example.com" +} +type File record {| + @Namespace { + uri: "example.com" + } + string fileName; + @Namespace { + uri: "example.com" + } + string fileNamespace; +|}; + +@test:Config { + groups: ["toXml"] +} +isolated function testToRecordFieldNameEndsWithNameOrNamespace() returns error? { + File file = { + fileName: "test.bal", + fileNamespace: "wso2.com" + }; + + xml result = check toXml(file); + string expected = "test.balwso2.com"; + test:assertEquals(result.toString(), expected); +} + @test:Config { groups: ["toXml"] } diff --git a/native/src/main/java/io/ballerina/lib/data/xmldata/utils/Constants.java b/native/src/main/java/io/ballerina/lib/data/xmldata/utils/Constants.java index 2e3e477..04830ac 100644 --- a/native/src/main/java/io/ballerina/lib/data/xmldata/utils/Constants.java +++ b/native/src/main/java/io/ballerina/lib/data/xmldata/utils/Constants.java @@ -42,6 +42,7 @@ private Constants() {} public static final ArrayType JSON_ARRAY_TYPE = TypeCreator.createArrayType(PredefinedTypes.TYPE_JSON); public static final String FIELD = "$field$."; public static final String NAMESPACE = "Namespace"; + public static final String MODULE_NAME = "ballerina/data.xmldata"; public static final BString URI = StringUtils.fromString("uri"); public static final BString PREFIX = StringUtils.fromString("prefix"); public static final String ATTRIBUTE = "Attribute"; @@ -58,6 +59,6 @@ private Constants() {} public static final BString ATTRIBUTE_PREFIX = StringUtils.fromString("attributePrefix"); public static final BString TEXT_FIELD_NAME = StringUtils.fromString("textFieldName"); public static final BString ALLOW_DATA_PROJECTION = StringUtils.fromString("allowDataProjection"); - public static final String NON_NUMERIC_STRING_REGEX = "[^a-zA-Z\\d\s]"; + public static final String RECORD_FIELD_NAME_ESCAPE_CHAR_REGEX = "[^a-zA-Z\\d\s_]"; public static final String NS_ANNOT_NOT_DEFINED = "$$ns_annot_not_defined$$"; } diff --git a/native/src/main/java/io/ballerina/lib/data/xmldata/utils/DataUtils.java b/native/src/main/java/io/ballerina/lib/data/xmldata/utils/DataUtils.java index dfe0d70..f9c3b95 100644 --- a/native/src/main/java/io/ballerina/lib/data/xmldata/utils/DataUtils.java +++ b/native/src/main/java/io/ballerina/lib/data/xmldata/utils/DataUtils.java @@ -66,8 +66,7 @@ public static QualifiedName validateAndGetXmlNameFromRecordAnnotation(RecordType BMap annotations = recordType.getAnnotations(); String localName = recordName; for (BString annotationsKey : annotations.getKeys()) { - String key = annotationsKey.getValue(); - if (!key.contains(Constants.FIELD) && key.endsWith(Constants.NAME)) { + if (isNameAnnotationKey(annotationsKey.getValue())) { String name = ((BMap) annotations.get(annotationsKey)).get(Constants.VALUE).toString(); String localPart = elementName.getLocalPart(); if (!name.equals(localPart)) { @@ -81,7 +80,7 @@ public static QualifiedName validateAndGetXmlNameFromRecordAnnotation(RecordType // Handle the namespace annotation. for (BString annotationsKey : annotations.getKeys()) { String key = annotationsKey.getValue(); - if (!key.contains(Constants.FIELD) && key.endsWith(Constants.NAMESPACE)) { + if (isNamespaceAnnotationKey(key)) { Map namespaceAnnotation = ((Map) annotations.get(StringUtils.fromString(key))); BString uri = (BString) namespaceAnnotation.get(Constants.URI); @@ -106,8 +105,7 @@ private static ArrayList getNamespace(RecordType recordType) { BMap annotations = recordType.getAnnotations(); ArrayList namespace = new ArrayList<>(); for (BString annotationsKey : annotations.getKeys()) { - String key = annotationsKey.getValue(); - if (!key.contains(Constants.FIELD) && key.endsWith(Constants.NAMESPACE)) { + if (isNamespaceAnnotationKey(annotationsKey.getValue())) { BMap namespaceAnnotation = (BMap) annotations.get(annotationsKey); namespace.add(namespaceAnnotation.containsKey(Constants.PREFIX) ? ((BString) namespaceAnnotation.get(Constants.PREFIX)).getValue() : ""); @@ -184,7 +182,7 @@ public static Map getAllAttributesInRecordType(RecordType @SuppressWarnings("unchecked") public static QualifiedName getFieldNameFromRecord(Map fieldAnnotation, String fieldName) { for (BString key : fieldAnnotation.keySet()) { - if (key.getValue().endsWith(Constants.NAMESPACE)) { + if (isNamespaceAnnotationKey(key.getValue())) { Map namespaceAnnotation = ((Map) fieldAnnotation.get(key)); BString uri = (BString) namespaceAnnotation.get(Constants.URI); BString prefix = (BString) namespaceAnnotation.get(Constants.PREFIX); @@ -198,7 +196,7 @@ public static QualifiedName getFieldNameFromRecord(Map fieldAnn @SuppressWarnings("unchecked") private static String getModifiedName(Map fieldAnnotation, String attributeName) { for (BString key : fieldAnnotation.keySet()) { - if (key.getValue().endsWith(Constants.NAME)) { + if (isNameAnnotationKey(key.getValue())) { return ((Map) fieldAnnotation.get(key)).get(Constants.VALUE).toString(); } } @@ -459,14 +457,28 @@ private static BMap addFields(BMap input, Type processRecord(key, annotations, recordValue, value, fieldType); } else if (fieldType.getTag() == TypeTags.TYPE_REFERENCED_TYPE_TAG) { Type referredType = TypeUtils.getReferredType(fieldType); + BMap namespaceAnnotRecord = ValueCreator.createMapValue(Constants.JSON_MAP_TYPE); + boolean doesNamespaceDefinedInField = false; if (annotations.size() > 0) { + String fieldName = key; key = getKeyNameFromAnnotation(annotations, key); + QName qName = addFieldNamespaceAnnotation(fieldName, key, annotations, namespaceAnnotRecord); + if (!qName.getNamespaceURI().equals("")) { + doesNamespaceDefinedInField = true; + } + String localPart = qName.getLocalPart(); + key = qName.getPrefix().isBlank() ? localPart : qName.getPrefix() + ":" + localPart; } - BMap subRecordAnnotations = ((RecordType) referredType).getAnnotations(); - key = getElementName(subRecordAnnotations, key); + BMap annotationRecord = ValueCreator.createMapValue(Constants.JSON_MAP_TYPE); - processSubRecordAnnotation(subRecordAnnotations, annotationRecord); + if (!doesNamespaceDefinedInField) { + BMap subRecordAnnotations = ((RecordType) referredType).getAnnotations(); + key = getElementName(subRecordAnnotations, key); + processSubRecordAnnotation(subRecordAnnotations, annotationRecord); + } + BMap subRecordValue = addFields(((BMap) value), referredType); + addNamespaceToSubRecord(key, namespaceAnnotRecord, subRecordValue); if (annotationRecord.size() > 0) { subRecordValue.put(annotationRecord.getKeys()[0], annotationRecord.get(annotationRecord.getKeys()[0])); @@ -475,7 +487,7 @@ private static BMap addFields(BMap input, Type } else if (fieldType.getTag() == TypeTags.ARRAY_TAG) { processArray(fieldType, annotations, recordValue, entry); } else { - addPrimitiveValue(addFieldNamespaceAnnotation(key, annotations, recordValue), + addPrimitiveValue(addFieldNamespaceAnnotation(key, key, annotations, recordValue), annotations, recordValue, value); } } else { @@ -486,15 +498,32 @@ private static BMap addFields(BMap input, Type } @SuppressWarnings("unchecked") - private static QName addFieldNamespaceAnnotation(String key, BMap annotations, + private static void addNamespaceToSubRecord(String key, BMap namespaceAnnotRecord, + BMap subRecord) { + if (namespaceAnnotRecord.isEmpty()) { + return; + } + + Object value = namespaceAnnotRecord.get(StringUtils.fromString(key)); + if (value == null) { + return; + } + + for (Map.Entry nsAnnotEntry: ((BMap) value).entrySet()) { + subRecord.put(nsAnnotEntry.getKey(), nsAnnotEntry.getValue()); + } + } + + @SuppressWarnings("unchecked") + private static QName addFieldNamespaceAnnotation(String fieldName, String key, BMap annotations, BMap recordValue) { BString annotationKey = StringUtils.fromString(Constants.FIELD - + (key.replaceAll(Constants.NON_NUMERIC_STRING_REGEX, "\\\\$0"))); + + (fieldName.replaceAll(Constants.RECORD_FIELD_NAME_ESCAPE_CHAR_REGEX, "\\\\$0"))); boolean isAttributeField = isAttributeField(annotationKey, annotations); if (annotations.containsKey(annotationKey)) { BMap annotationValue = (BMap) annotations.get(annotationKey); for (BString fieldKey : annotationValue.getKeys()) { - if (fieldKey.toString().endsWith(Constants.NAMESPACE)) { + if (isNamespaceAnnotationKey(fieldKey.getValue())) { return processFieldNamespaceAnnotation(annotationValue, key, fieldKey, recordValue, isAttributeField); } @@ -511,7 +540,7 @@ public static boolean isAttributeField(BString annotationKey, BMap annotationValue = (BMap) annotations.get(annotationKey); for (BString fieldKey : annotationValue.getKeys()) { - if (fieldKey.toString().endsWith(Constants.ATTRIBUTE)) { + if (isAttributeAnnotationKey(fieldKey.getValue())) { return true; } } @@ -524,7 +553,7 @@ private static BMap getFieldNamespaceAndNameAnnotations(String BMap nsFieldAnnotation = ValueCreator.createMapValue(Constants.JSON_MAP_TYPE); BString annotationKey = StringUtils.fromString((Constants.FIELD - + (key.replaceAll(Constants.NON_NUMERIC_STRING_REGEX, "\\\\$0")))); + + (key.replaceAll(Constants.RECORD_FIELD_NAME_ESCAPE_CHAR_REGEX, "\\\\$0")))); if (!parentAnnotations.containsKey(annotationKey)) { return nsFieldAnnotation; } @@ -532,7 +561,7 @@ private static BMap getFieldNamespaceAndNameAnnotations(String BMap annotationValue = (BMap) parentAnnotations.get(annotationKey); for (BString fieldKey : annotationValue.getKeys()) { String keyName = fieldKey.getValue(); - if (keyName.endsWith(Constants.NAMESPACE) || keyName.endsWith(Constants.NAME)) { + if (isNamespaceAnnotationKey(keyName) || isNameAnnotationKey(keyName)) { nsFieldAnnotation.put(fieldKey, annotationValue.get(fieldKey)); break; } @@ -568,7 +597,7 @@ private static void addPrimitiveValue(QName qName, BMap annotat BString key = qName.getPrefix().isBlank() ? localPart : StringUtils.fromString(qName.getPrefix() + ":" + localPart); BString annotationKey = StringUtils.fromString(Constants.FIELD - + (localPart.getValue().replaceAll(Constants.NON_NUMERIC_STRING_REGEX, "\\\\$0"))); + + (localPart.getValue().replaceAll(Constants.RECORD_FIELD_NAME_ESCAPE_CHAR_REGEX, "\\\\$0"))); BMap currentValue; if (record.containsKey(key)) { currentValue = (BMap) record.get(key); @@ -625,7 +654,7 @@ private static void processArray(Type childType, BMap annotatio @SuppressWarnings("unchecked") private static String getKeyNameFromAnnotation(BMap annotations, String keyName) { BString annotationKey = StringUtils.fromString(Constants.FIELD - + (keyName.replaceAll(Constants.NON_NUMERIC_STRING_REGEX, "\\\\$0"))); + + (keyName.replaceAll(Constants.RECORD_FIELD_NAME_ESCAPE_CHAR_REGEX, "\\\\$0"))); if (annotations.containsKey(annotationKey)) { BMap annotationValue = (BMap) annotations.get(annotationKey); return processFieldAnnotation(annotationValue, keyName); @@ -663,7 +692,7 @@ private static BMap processParentAnnotation(Type type, BMap annotation, String key) { for (BString value : annotation.getKeys()) { String stringValue = value.getValue(); - if (stringValue.endsWith(Constants.NAME)) { + if (isNameAnnotationKey(stringValue)) { BMap names = (BMap) annotation.get(value); String name = names.get(StringUtils.fromString(VALUE)).toString(); if (key.contains(Constants.COLON)) { @@ -674,7 +703,7 @@ private static String processFieldAnnotation(BMap annotation, S key = name; } } - if (stringValue.endsWith(Constants.ATTRIBUTE)) { + if (isAttributeAnnotationKey(stringValue)) { key = ATTRIBUTE_PREFIX.concat(key); } } @@ -687,10 +716,10 @@ private static BString processAnnotation(BMap annotation, Strin for (BString value : annotation.getKeys()) { if (!value.getValue().contains(Constants.FIELD)) { String stringValue = value.getValue(); - if (stringValue.endsWith(Constants.NAME)) { + if (isNameAnnotationKey(stringValue)) { key = processNameAnnotation(annotation, key, value, hasNamespaceAnnotation); } - if (stringValue.endsWith(Constants.NAMESPACE)) { + if (isNamespaceAnnotationKey(stringValue)) { hasNamespaceAnnotation = true; key = processNamespaceAnnotation(annotation, key, value, namespaces); } @@ -702,7 +731,7 @@ private static BString processAnnotation(BMap annotation, Strin private static void processSubRecordAnnotation(BMap annotation, BMap subRecord) { BString[] keys = annotation.getKeys(); for (BString value : keys) { - if (value.getValue().endsWith(Constants.NAMESPACE)) { + if (isNamespaceAnnotationKey(value.getValue())) { processNamespaceAnnotation(annotation, "", value, subRecord); } } @@ -713,7 +742,7 @@ private static String getElementName(BMap annotation, String ke BString[] keys = annotation.getKeys(); boolean hasNamespaceAnnotation = false; for (BString value : keys) { - if (value.getValue().endsWith(Constants.NAMESPACE)) { + if (isNamespaceAnnotationKey(value.getValue())) { hasNamespaceAnnotation = true; BMap namespaceAnnotation = (BMap) annotation.get(value); BString prefix = (BString) namespaceAnnotation.get(Constants.PREFIX); @@ -721,7 +750,10 @@ private static String getElementName(BMap annotation, String ke key = prefix.getValue().concat(Constants.COLON).concat(key); } } - if (value.getValue().endsWith(Constants.NAME)) { + } + + for (BString value : keys) { + if (isNameAnnotationKey(value.getValue())) { key = processNameAnnotation(annotation, key, value, hasNamespaceAnnotation); } } @@ -782,6 +814,18 @@ private static String addAttributeToRecord(BString prefix, BString uri, String k return prefix.getValue().concat(Constants.COLON).concat(key); } + private static boolean isNamespaceAnnotationKey(String key) { + return key.startsWith(Constants.MODULE_NAME) && key.endsWith(Constants.NAMESPACE); + } + + private static boolean isNameAnnotationKey(String key) { + return key.startsWith(Constants.MODULE_NAME) && key.endsWith(Constants.NAME); + } + + private static boolean isAttributeAnnotationKey(String key) { + return key.startsWith(Constants.MODULE_NAME) && key.endsWith(Constants.ATTRIBUTE); + } + /** * Holds data required for the traversing. *