Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added type conversion support #540

Merged
merged 7 commits into from
Sep 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/main/java/org/json/XML.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ of this software and associated documentation files (the "Software"), to deal

import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Iterator;


/**
* This provides static methods to convert an XML text into a JSONObject, and to
* covert a JSONObject into an XML text.
Expand Down Expand Up @@ -72,6 +74,8 @@ public class XML {
*/
public static final String NULL_ATTR = "xsi:nil";

public static final String TYPE_ATTR = "xsi:type";

/**
* Creates an iterator for navigating Code Points in a string instead of
* characters. Once Java7 support is dropped, this can be replaced with
Expand Down Expand Up @@ -257,6 +261,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
String string;
String tagName;
Object token;
XMLXsiTypeConverter<?> xmlXsiTypeConverter;

// Test for and skip past these forms:
// <!-- ... -->
Expand Down Expand Up @@ -336,6 +341,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
token = null;
jsonObject = new JSONObject();
boolean nilAttributeFound = false;
xmlXsiTypeConverter = null;
for (;;) {
if (token == null) {
token = x.nextToken();
Expand All @@ -354,6 +360,9 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
&& NULL_ATTR.equals(string)
&& Boolean.parseBoolean((String) token)) {
nilAttributeFound = true;
} else if(config.getXsiTypeMap() != null && !config.getXsiTypeMap().isEmpty()
&& TYPE_ATTR.equals(string)) {
xmlXsiTypeConverter = config.getXsiTypeMap().get(token);
} else if (!nilAttributeFound) {
jsonObject.accumulate(string,
config.isKeepStrings()
Expand Down Expand Up @@ -392,8 +401,13 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
} else if (token instanceof String) {
string = (String) token;
if (string.length() > 0) {
jsonObject.accumulate(config.getcDataTagName(),
config.isKeepStrings() ? string : stringToValue(string));
if(xmlXsiTypeConverter != null) {
jsonObject.accumulate(config.getcDataTagName(),
stringToValue(string, xmlXsiTypeConverter));
} else {
jsonObject.accumulate(config.getcDataTagName(),
config.isKeepStrings() ? string : stringToValue(string));
}
}

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

/**
* This method tries to convert the given string value to the target object
* @param string String to convert
* @param typeConverter value converter to convert string to integer, boolean e.t.c
* @return JSON value of this string or the string
*/
public static Object stringToValue(String string, XMLXsiTypeConverter<?> typeConverter) {
if(typeConverter != null) {
return typeConverter.convert(string);
}
return stringToValue(string);
}

/**
* This method is the same as {@link JSONObject#stringToValue(String)}.
*
Expand Down
62 changes: 60 additions & 2 deletions src/main/java/org/json/XMLParserConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ of this software and associated documentation files (the "Software"), to deal
SOFTWARE.
*/

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;


/**
* Configuration object for the XML parser. The configuration is immutable.
* @author AylwardJ
Expand Down Expand Up @@ -56,6 +61,11 @@ public class XMLParserConfiguration {
*/
private boolean convertNilAttributeToNull;

/**
* This will allow type conversion for values in XML if xsi:type attribute is defined
*/
private Map<String, XMLXsiTypeConverter<?>> xsiTypeMap;

/**
* Default parser configuration. Does not keep strings (tries to implicitly convert
* values), and the CDATA Tag Name is "content".
kumar529 marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -64,6 +74,7 @@ public XMLParserConfiguration () {
this.keepStrings = false;
this.cDataTagName = "content";
this.convertNilAttributeToNull = false;
this.xsiTypeMap = Collections.emptyMap();
}

/**
Expand Down Expand Up @@ -129,7 +140,26 @@ public XMLParserConfiguration (final boolean keepStrings, final String cDataTagN
this.cDataTagName = cDataTagName;
this.convertNilAttributeToNull = convertNilAttributeToNull;
}


/**
* Configure the parser to use custom settings.
* @param keepStrings <code>true</code> to parse all values as string.
* <code>false</code> to try and convert XML string values into a JSON value.
* @param cDataTagName <code>null</code> to disable CDATA processing. Any other value
* to use that value as the JSONObject key name to process as CDATA.
* @param convertNilAttributeToNull <code>true</code> to parse values with attribute xsi:nil="true" as null.
* <code>false</code> to parse values with attribute xsi:nil="true" as {"xsi:nil":true}.
* @param xsiTypeMap <code>new HashMap<String, XMLXsiTypeConverter<?>>()</code> to parse values with attribute
* xsi:type="integer" as integer, xsi:type="string" as string
*/
private XMLParserConfiguration (final boolean keepStrings, final String cDataTagName,
final boolean convertNilAttributeToNull, final Map<String, XMLXsiTypeConverter<?>> xsiTypeMap ) {
this.keepStrings = keepStrings;
this.cDataTagName = cDataTagName;
this.convertNilAttributeToNull = convertNilAttributeToNull;
this.xsiTypeMap = Collections.unmodifiableMap(xsiTypeMap);
}

/**
* Provides a new instance of the same configuration.
*/
Expand All @@ -143,7 +173,8 @@ protected XMLParserConfiguration clone() {
return new XMLParserConfiguration(
this.keepStrings,
this.cDataTagName,
this.convertNilAttributeToNull
this.convertNilAttributeToNull,
this.xsiTypeMap
);
}

Expand Down Expand Up @@ -225,4 +256,31 @@ public XMLParserConfiguration withConvertNilAttributeToNull(final boolean newVal
newConfig.convertNilAttributeToNull = newVal;
return newConfig;
}

/**
* When parsing the XML into JSON, specifies that the values with attribute xsi:type
* will be converted to target type defined to client in this configuration
* {@code Map<String, XMLXsiTypeConverter<?>>} to parse values with attribute
* xsi:type="integer" as integer, xsi:type="string" as string
* @return {@link #xsiTypeMap} unmodifiable configuration map.
*/
public Map<String, XMLXsiTypeConverter<?>> getXsiTypeMap() {
return this.xsiTypeMap;
}

/**
* When parsing the XML into JSON, specifies that the values with attribute xsi:type
* will be converted to target type defined to client in this configuration
* {@code Map<String, XMLXsiTypeConverter<?>>} to parse values with attribute
* xsi:type="integer" as integer, xsi:type="string" as string
* @param xsiTypeMap {@code new HashMap<String, XMLXsiTypeConverter<?>>()} to parse values with attribute
* xsi:type="integer" as integer, xsi:type="string" as string
* @return The existing configuration will not be modified. A new configuration is returned.
*/
public XMLParserConfiguration withXsiTypeMap(final Map<String, XMLXsiTypeConverter<?>> xsiTypeMap) {
XMLParserConfiguration newConfig = this.clone();
Map<String, XMLXsiTypeConverter<?>> cloneXsiTypeMap = new HashMap<String, XMLXsiTypeConverter<?>>(xsiTypeMap);
newConfig.xsiTypeMap = Collections.unmodifiableMap(cloneXsiTypeMap);
return newConfig;
}
}
66 changes: 66 additions & 0 deletions src/main/java/org/json/XMLXsiTypeConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.json;
kumar529 marked this conversation as resolved.
Show resolved Hide resolved
/*
Copyright (c) 2002 JSON.org

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

The Software shall be used for Good, not Evil.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

/**
* Type conversion configuration interface to be used with xsi:type attributes.
* <pre>
* <b>XML Sample</b>
* {@code
* <root>
* <asString xsi:type="string">12345</asString>
* <asInt xsi:type="integer">54321</asInt>
* </root>
* }
* <b>JSON Output</b>
* {@code
* {
* "root" : {
* "asString" : "12345",
* "asInt": 54321
* }
* }
* }
*
* <b>Usage</b>
* {@code
* Map<String, XMLXsiTypeConverter<?>> xsiTypeMap = new HashMap<String, XMLXsiTypeConverter<?>>();
* xsiTypeMap.put("string", new XMLXsiTypeConverter<String>() {
* &#64;Override public String convert(final String value) {
* return value;
* }
* });
* xsiTypeMap.put("integer", new XMLXsiTypeConverter<Integer>() {
* &#64;Override public Integer convert(final String value) {
* return Integer.valueOf(value);
* }
* });
* }
* </pre>
* @author kumar529
* @param <T> return type of convert method
*/
public interface XMLXsiTypeConverter<T> {
T convert(String value);
}
99 changes: 97 additions & 2 deletions src/test/java/org/json/junit/XMLTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ of this software and associated documentation files (the "Software"), to deal
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.json.XML;
import org.json.XMLParserConfiguration;
import org.json.XMLXsiTypeConverter;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
Expand Down Expand Up @@ -972,5 +975,97 @@ public void testIssue537CaseSensitiveHexUnEscapeDirect(){

assertEquals("Case insensitive Entity unescape", expectedStr, actualStr);
}

}

/**
* test passes when xsi:type="java.lang.String" not converting to string
*/
@Test
public void testToJsonWithTypeWhenTypeConversionDisabled() {
String originalXml = "<root><id xsi:type=\"string\">1234</id></root>";
String expectedJsonString = "{\"root\":{\"id\":{\"xsi:type\":\"string\",\"content\":1234}}}";
JSONObject expectedJson = new JSONObject(expectedJsonString);
JSONObject actualJson = XML.toJSONObject(originalXml, new XMLParserConfiguration());
Util.compareActualVsExpectedJsonObjects(actualJson,expectedJson);
}

/**
* test passes when xsi:type="java.lang.String" converting to String
*/
@Test
public void testToJsonWithTypeWhenTypeConversionEnabled() {
String originalXml = "<root><id1 xsi:type=\"string\">1234</id1>"
+ "<id2 xsi:type=\"integer\">1234</id2></root>";
String expectedJsonString = "{\"root\":{\"id2\":1234,\"id1\":\"1234\"}}";
JSONObject expectedJson = new JSONObject(expectedJsonString);
Map<String, XMLXsiTypeConverter<?>> xsiTypeMap = new HashMap<String, XMLXsiTypeConverter<?>>();
xsiTypeMap.put("string", new XMLXsiTypeConverter<String>() {
@Override public String convert(final String value) {
return value;
}
});
xsiTypeMap.put("integer", new XMLXsiTypeConverter<Integer>() {
@Override public Integer convert(final String value) {
return Integer.valueOf(value);
}
});
JSONObject actualJson = XML.toJSONObject(originalXml, new XMLParserConfiguration().withXsiTypeMap(xsiTypeMap));
Util.compareActualVsExpectedJsonObjects(actualJson,expectedJson);
}

@Test
public void testToJsonWithXSITypeWhenTypeConversionEnabled() {
String originalXml = "<root><asString xsi:type=\"string\">12345</asString><asInt "
+ "xsi:type=\"integer\">54321</asInt></root>";
String expectedJsonString = "{\"root\":{\"asString\":\"12345\",\"asInt\":54321}}";
JSONObject expectedJson = new JSONObject(expectedJsonString);
Map<String, XMLXsiTypeConverter<?>> xsiTypeMap = new HashMap<String, XMLXsiTypeConverter<?>>();
xsiTypeMap.put("string", new XMLXsiTypeConverter<String>() {
@Override public String convert(final String value) {
return value;
}
});
xsiTypeMap.put("integer", new XMLXsiTypeConverter<Integer>() {
@Override public Integer convert(final String value) {
return Integer.valueOf(value);
}
});
JSONObject actualJson = XML.toJSONObject(originalXml, new XMLParserConfiguration().withXsiTypeMap(xsiTypeMap));
Util.compareActualVsExpectedJsonObjects(actualJson,expectedJson);
}

@Test
public void testToJsonWithXSITypeWhenTypeConversionNotEnabledOnOne() {
String originalXml = "<root><asString xsi:type=\"string\">12345</asString><asInt>54321</asInt></root>";
String expectedJsonString = "{\"root\":{\"asString\":\"12345\",\"asInt\":54321}}";
JSONObject expectedJson = new JSONObject(expectedJsonString);
Map<String, XMLXsiTypeConverter<?>> xsiTypeMap = new HashMap<String, XMLXsiTypeConverter<?>>();
xsiTypeMap.put("string", new XMLXsiTypeConverter<String>() {
@Override public String convert(final String value) {
return value;
}
});
JSONObject actualJson = XML.toJSONObject(originalXml, new XMLParserConfiguration().withXsiTypeMap(xsiTypeMap));
Util.compareActualVsExpectedJsonObjects(actualJson,expectedJson);
}

@Test
public void testXSITypeMapNotModifiable() {
Map<String, XMLXsiTypeConverter<?>> xsiTypeMap = new HashMap<String, XMLXsiTypeConverter<?>>();
XMLParserConfiguration config = new XMLParserConfiguration().withXsiTypeMap(xsiTypeMap);
xsiTypeMap.put("string", new XMLXsiTypeConverter<String>() {
@Override public String convert(final String value) {
return value;
}
});
assertEquals("Config Conversion Map size is expected to be 0", 0, config.getXsiTypeMap().size());

try {
config.getXsiTypeMap().put("boolean", new XMLXsiTypeConverter<Boolean>() {
@Override public Boolean convert(final String value) {
return Boolean.valueOf(value);
}
});
fail("Expected to be unable to modify the config");
} catch (Exception ignored) { }
}
}