Skip to content

Commit 7abd89b

Browse files
authored
Merge pull request #540 from kumar529/master
Added type conversion support
2 parents 5541a6d + 56d4130 commit 7abd89b

4 files changed

Lines changed: 252 additions & 6 deletions

File tree

src/main/java/org/json/XML.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ of this software and associated documentation files (the "Software"), to deal
2626

2727
import java.io.Reader;
2828
import java.io.StringReader;
29+
import java.lang.reflect.Method;
2930
import java.math.BigDecimal;
3031
import java.math.BigInteger;
3132
import java.util.Iterator;
3233

34+
3335
/**
3436
* This provides static methods to convert an XML text into a JSONObject, and to
3537
* covert a JSONObject into an XML text.
@@ -72,6 +74,8 @@ public class XML {
7274
*/
7375
public static final String NULL_ATTR = "xsi:nil";
7476

77+
public static final String TYPE_ATTR = "xsi:type";
78+
7579
/**
7680
* Creates an iterator for navigating Code Points in a string instead of
7781
* characters. Once Java7 support is dropped, this can be replaced with
@@ -257,6 +261,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
257261
String string;
258262
String tagName;
259263
Object token;
264+
XMLXsiTypeConverter<?> xmlXsiTypeConverter;
260265

261266
// Test for and skip past these forms:
262267
// <!-- ... -->
@@ -336,6 +341,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
336341
token = null;
337342
jsonObject = new JSONObject();
338343
boolean nilAttributeFound = false;
344+
xmlXsiTypeConverter = null;
339345
for (;;) {
340346
if (token == null) {
341347
token = x.nextToken();
@@ -354,6 +360,9 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
354360
&& NULL_ATTR.equals(string)
355361
&& Boolean.parseBoolean((String) token)) {
356362
nilAttributeFound = true;
363+
} else if(config.getXsiTypeMap() != null && !config.getXsiTypeMap().isEmpty()
364+
&& TYPE_ATTR.equals(string)) {
365+
xmlXsiTypeConverter = config.getXsiTypeMap().get(token);
357366
} else if (!nilAttributeFound) {
358367
jsonObject.accumulate(string,
359368
config.isKeepStrings()
@@ -392,8 +401,13 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
392401
} else if (token instanceof String) {
393402
string = (String) token;
394403
if (string.length() > 0) {
395-
jsonObject.accumulate(config.getcDataTagName(),
396-
config.isKeepStrings() ? string : stringToValue(string));
404+
if(xmlXsiTypeConverter != null) {
405+
jsonObject.accumulate(config.getcDataTagName(),
406+
stringToValue(string, xmlXsiTypeConverter));
407+
} else {
408+
jsonObject.accumulate(config.getcDataTagName(),
409+
config.isKeepStrings() ? string : stringToValue(string));
410+
}
397411
}
398412

399413
} else if (token == LT) {
@@ -418,6 +432,19 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
418432
}
419433
}
420434

435+
/**
436+
* This method tries to convert the given string value to the target object
437+
* @param string String to convert
438+
* @param typeConverter value converter to convert string to integer, boolean e.t.c
439+
* @return JSON value of this string or the string
440+
*/
441+
public static Object stringToValue(String string, XMLXsiTypeConverter<?> typeConverter) {
442+
if(typeConverter != null) {
443+
return typeConverter.convert(string);
444+
}
445+
return stringToValue(string);
446+
}
447+
421448
/**
422449
* This method is the same as {@link JSONObject#stringToValue(String)}.
423450
*

src/main/java/org/json/XMLParserConfiguration.java

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ of this software and associated documentation files (the "Software"), to deal
2323
SOFTWARE.
2424
*/
2525

26+
import java.util.Collections;
27+
import java.util.HashMap;
28+
import java.util.Map;
29+
30+
2631
/**
2732
* Configuration object for the XML parser. The configuration is immutable.
2833
* @author AylwardJ
@@ -56,6 +61,11 @@ public class XMLParserConfiguration {
5661
*/
5762
private boolean convertNilAttributeToNull;
5863

64+
/**
65+
* This will allow type conversion for values in XML if xsi:type attribute is defined
66+
*/
67+
private Map<String, XMLXsiTypeConverter<?>> xsiTypeMap;
68+
5969
/**
6070
* Default parser configuration. Does not keep strings (tries to implicitly convert
6171
* values), and the CDATA Tag Name is "content".
@@ -64,6 +74,7 @@ public XMLParserConfiguration () {
6474
this.keepStrings = false;
6575
this.cDataTagName = "content";
6676
this.convertNilAttributeToNull = false;
77+
this.xsiTypeMap = Collections.emptyMap();
6778
}
6879

6980
/**
@@ -129,7 +140,26 @@ public XMLParserConfiguration (final boolean keepStrings, final String cDataTagN
129140
this.cDataTagName = cDataTagName;
130141
this.convertNilAttributeToNull = convertNilAttributeToNull;
131142
}
132-
143+
144+
/**
145+
* Configure the parser to use custom settings.
146+
* @param keepStrings <code>true</code> to parse all values as string.
147+
* <code>false</code> to try and convert XML string values into a JSON value.
148+
* @param cDataTagName <code>null</code> to disable CDATA processing. Any other value
149+
* to use that value as the JSONObject key name to process as CDATA.
150+
* @param convertNilAttributeToNull <code>true</code> to parse values with attribute xsi:nil="true" as null.
151+
* <code>false</code> to parse values with attribute xsi:nil="true" as {"xsi:nil":true}.
152+
* @param xsiTypeMap <code>new HashMap<String, XMLXsiTypeConverter<?>>()</code> to parse values with attribute
153+
* xsi:type="integer" as integer, xsi:type="string" as string
154+
*/
155+
private XMLParserConfiguration (final boolean keepStrings, final String cDataTagName,
156+
final boolean convertNilAttributeToNull, final Map<String, XMLXsiTypeConverter<?>> xsiTypeMap ) {
157+
this.keepStrings = keepStrings;
158+
this.cDataTagName = cDataTagName;
159+
this.convertNilAttributeToNull = convertNilAttributeToNull;
160+
this.xsiTypeMap = Collections.unmodifiableMap(xsiTypeMap);
161+
}
162+
133163
/**
134164
* Provides a new instance of the same configuration.
135165
*/
@@ -143,7 +173,8 @@ protected XMLParserConfiguration clone() {
143173
return new XMLParserConfiguration(
144174
this.keepStrings,
145175
this.cDataTagName,
146-
this.convertNilAttributeToNull
176+
this.convertNilAttributeToNull,
177+
this.xsiTypeMap
147178
);
148179
}
149180

@@ -225,4 +256,31 @@ public XMLParserConfiguration withConvertNilAttributeToNull(final boolean newVal
225256
newConfig.convertNilAttributeToNull = newVal;
226257
return newConfig;
227258
}
259+
260+
/**
261+
* When parsing the XML into JSON, specifies that the values with attribute xsi:type
262+
* will be converted to target type defined to client in this configuration
263+
* {@code Map<String, XMLXsiTypeConverter<?>>} to parse values with attribute
264+
* xsi:type="integer" as integer, xsi:type="string" as string
265+
* @return {@link #xsiTypeMap} unmodifiable configuration map.
266+
*/
267+
public Map<String, XMLXsiTypeConverter<?>> getXsiTypeMap() {
268+
return this.xsiTypeMap;
269+
}
270+
271+
/**
272+
* When parsing the XML into JSON, specifies that the values with attribute xsi:type
273+
* will be converted to target type defined to client in this configuration
274+
* {@code Map<String, XMLXsiTypeConverter<?>>} to parse values with attribute
275+
* xsi:type="integer" as integer, xsi:type="string" as string
276+
* @param xsiTypeMap {@code new HashMap<String, XMLXsiTypeConverter<?>>()} to parse values with attribute
277+
* xsi:type="integer" as integer, xsi:type="string" as string
278+
* @return The existing configuration will not be modified. A new configuration is returned.
279+
*/
280+
public XMLParserConfiguration withXsiTypeMap(final Map<String, XMLXsiTypeConverter<?>> xsiTypeMap) {
281+
XMLParserConfiguration newConfig = this.clone();
282+
Map<String, XMLXsiTypeConverter<?>> cloneXsiTypeMap = new HashMap<String, XMLXsiTypeConverter<?>>(xsiTypeMap);
283+
newConfig.xsiTypeMap = Collections.unmodifiableMap(cloneXsiTypeMap);
284+
return newConfig;
285+
}
228286
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.json;
2+
/*
3+
Copyright (c) 2002 JSON.org
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
The Software shall be used for Good, not Evil.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
*/
25+
26+
/**
27+
* Type conversion configuration interface to be used with xsi:type attributes.
28+
* <pre>
29+
* <b>XML Sample</b>
30+
* {@code
31+
* <root>
32+
* <asString xsi:type="string">12345</asString>
33+
* <asInt xsi:type="integer">54321</asInt>
34+
* </root>
35+
* }
36+
* <b>JSON Output</b>
37+
* {@code
38+
* {
39+
* "root" : {
40+
* "asString" : "12345",
41+
* "asInt": 54321
42+
* }
43+
* }
44+
* }
45+
*
46+
* <b>Usage</b>
47+
* {@code
48+
* Map<String, XMLXsiTypeConverter<?>> xsiTypeMap = new HashMap<String, XMLXsiTypeConverter<?>>();
49+
* xsiTypeMap.put("string", new XMLXsiTypeConverter<String>() {
50+
* &#64;Override public String convert(final String value) {
51+
* return value;
52+
* }
53+
* });
54+
* xsiTypeMap.put("integer", new XMLXsiTypeConverter<Integer>() {
55+
* &#64;Override public Integer convert(final String value) {
56+
* return Integer.valueOf(value);
57+
* }
58+
* });
59+
* }
60+
* </pre>
61+
* @author kumar529
62+
* @param <T> return type of convert method
63+
*/
64+
public interface XMLXsiTypeConverter<T> {
65+
T convert(String value);
66+
}

src/test/java/org/json/junit/XMLTest.java

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,16 @@ of this software and associated documentation files (the "Software"), to deal
3838
import java.io.InputStreamReader;
3939
import java.io.Reader;
4040
import java.io.StringReader;
41+
import java.util.HashMap;
42+
import java.util.Map;
4143

4244
import org.json.JSONArray;
4345
import org.json.JSONException;
4446
import org.json.JSONObject;
4547
import org.json.JSONTokener;
4648
import org.json.XML;
4749
import org.json.XMLParserConfiguration;
50+
import org.json.XMLXsiTypeConverter;
4851
import org.junit.Rule;
4952
import org.junit.Test;
5053
import org.junit.rules.TemporaryFolder;
@@ -972,5 +975,97 @@ public void testIssue537CaseSensitiveHexUnEscapeDirect(){
972975

973976
assertEquals("Case insensitive Entity unescape", expectedStr, actualStr);
974977
}
975-
976-
}
978+
979+
/**
980+
* test passes when xsi:type="java.lang.String" not converting to string
981+
*/
982+
@Test
983+
public void testToJsonWithTypeWhenTypeConversionDisabled() {
984+
String originalXml = "<root><id xsi:type=\"string\">1234</id></root>";
985+
String expectedJsonString = "{\"root\":{\"id\":{\"xsi:type\":\"string\",\"content\":1234}}}";
986+
JSONObject expectedJson = new JSONObject(expectedJsonString);
987+
JSONObject actualJson = XML.toJSONObject(originalXml, new XMLParserConfiguration());
988+
Util.compareActualVsExpectedJsonObjects(actualJson,expectedJson);
989+
}
990+
991+
/**
992+
* test passes when xsi:type="java.lang.String" converting to String
993+
*/
994+
@Test
995+
public void testToJsonWithTypeWhenTypeConversionEnabled() {
996+
String originalXml = "<root><id1 xsi:type=\"string\">1234</id1>"
997+
+ "<id2 xsi:type=\"integer\">1234</id2></root>";
998+
String expectedJsonString = "{\"root\":{\"id2\":1234,\"id1\":\"1234\"}}";
999+
JSONObject expectedJson = new JSONObject(expectedJsonString);
1000+
Map<String, XMLXsiTypeConverter<?>> xsiTypeMap = new HashMap<String, XMLXsiTypeConverter<?>>();
1001+
xsiTypeMap.put("string", new XMLXsiTypeConverter<String>() {
1002+
@Override public String convert(final String value) {
1003+
return value;
1004+
}
1005+
});
1006+
xsiTypeMap.put("integer", new XMLXsiTypeConverter<Integer>() {
1007+
@Override public Integer convert(final String value) {
1008+
return Integer.valueOf(value);
1009+
}
1010+
});
1011+
JSONObject actualJson = XML.toJSONObject(originalXml, new XMLParserConfiguration().withXsiTypeMap(xsiTypeMap));
1012+
Util.compareActualVsExpectedJsonObjects(actualJson,expectedJson);
1013+
}
1014+
1015+
@Test
1016+
public void testToJsonWithXSITypeWhenTypeConversionEnabled() {
1017+
String originalXml = "<root><asString xsi:type=\"string\">12345</asString><asInt "
1018+
+ "xsi:type=\"integer\">54321</asInt></root>";
1019+
String expectedJsonString = "{\"root\":{\"asString\":\"12345\",\"asInt\":54321}}";
1020+
JSONObject expectedJson = new JSONObject(expectedJsonString);
1021+
Map<String, XMLXsiTypeConverter<?>> xsiTypeMap = new HashMap<String, XMLXsiTypeConverter<?>>();
1022+
xsiTypeMap.put("string", new XMLXsiTypeConverter<String>() {
1023+
@Override public String convert(final String value) {
1024+
return value;
1025+
}
1026+
});
1027+
xsiTypeMap.put("integer", new XMLXsiTypeConverter<Integer>() {
1028+
@Override public Integer convert(final String value) {
1029+
return Integer.valueOf(value);
1030+
}
1031+
});
1032+
JSONObject actualJson = XML.toJSONObject(originalXml, new XMLParserConfiguration().withXsiTypeMap(xsiTypeMap));
1033+
Util.compareActualVsExpectedJsonObjects(actualJson,expectedJson);
1034+
}
1035+
1036+
@Test
1037+
public void testToJsonWithXSITypeWhenTypeConversionNotEnabledOnOne() {
1038+
String originalXml = "<root><asString xsi:type=\"string\">12345</asString><asInt>54321</asInt></root>";
1039+
String expectedJsonString = "{\"root\":{\"asString\":\"12345\",\"asInt\":54321}}";
1040+
JSONObject expectedJson = new JSONObject(expectedJsonString);
1041+
Map<String, XMLXsiTypeConverter<?>> xsiTypeMap = new HashMap<String, XMLXsiTypeConverter<?>>();
1042+
xsiTypeMap.put("string", new XMLXsiTypeConverter<String>() {
1043+
@Override public String convert(final String value) {
1044+
return value;
1045+
}
1046+
});
1047+
JSONObject actualJson = XML.toJSONObject(originalXml, new XMLParserConfiguration().withXsiTypeMap(xsiTypeMap));
1048+
Util.compareActualVsExpectedJsonObjects(actualJson,expectedJson);
1049+
}
1050+
1051+
@Test
1052+
public void testXSITypeMapNotModifiable() {
1053+
Map<String, XMLXsiTypeConverter<?>> xsiTypeMap = new HashMap<String, XMLXsiTypeConverter<?>>();
1054+
XMLParserConfiguration config = new XMLParserConfiguration().withXsiTypeMap(xsiTypeMap);
1055+
xsiTypeMap.put("string", new XMLXsiTypeConverter<String>() {
1056+
@Override public String convert(final String value) {
1057+
return value;
1058+
}
1059+
});
1060+
assertEquals("Config Conversion Map size is expected to be 0", 0, config.getXsiTypeMap().size());
1061+
1062+
try {
1063+
config.getXsiTypeMap().put("boolean", new XMLXsiTypeConverter<Boolean>() {
1064+
@Override public Boolean convert(final String value) {
1065+
return Boolean.valueOf(value);
1066+
}
1067+
});
1068+
fail("Expected to be unable to modify the config");
1069+
} catch (Exception ignored) { }
1070+
}
1071+
}

0 commit comments

Comments
 (0)