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 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/main/java/org/json/XML.java
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.xsiTypeMap != null
&& TYPE_ATTR.equals(string)) {
xmlXsiTypeConverter = config.xsiTypeMap.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
30 changes: 27 additions & 3 deletions src/main/java/org/json/XMLParserConfiguration.java
Expand Up @@ -23,6 +23,9 @@ of this software and associated documentation files (the "Software"), to deal
SOFTWARE.
*/

import java.util.Map;


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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I finally had time for a closer look at the configuration changes. With the new changes in #543, this should be private with a Getter and no Setter. Preferably the getter should return an "Unmodifiable Map". To prevent wrapping the collection every time the getter is called, storing the unmodifiable map in this variable would be acceptable.

Note in the Getter javadoc that the map is "unmodifiable"


/**
* 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 Down Expand Up @@ -106,9 +114,7 @@ public XMLParserConfiguration (final String cDataTagName) {
*/
@Deprecated
public XMLParserConfiguration (final boolean keepStrings, final String cDataTagName) {
this.keepStrings = keepStrings;
this.cDataTagName = cDataTagName;
this.convertNilAttributeToNull = false;
this(keepStrings, cDataTagName, false);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not modify deprecated functions, or use them in your tests.

}

/**
Expand All @@ -125,9 +131,27 @@ public XMLParserConfiguration (final boolean keepStrings, final String cDataTagN
*/
@Deprecated
public XMLParserConfiguration (final boolean keepStrings, final String cDataTagName, final boolean convertNilAttributeToNull) {
this(keepStrings, cDataTagName, convertNilAttributeToNull, null);
}

/**
* 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
* <code>null</code> to use default behaviour.
*/
public XMLParserConfiguration (final boolean keepStrings, final String cDataTagName,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be marked private instead of public.
Also, please change:

this.xsiTypeMap = xsiTypeMap

to:

this.xsiTypeMap = Collections.unmodifiableMap(new HashMap<String, XMLXsiTypeConverter<?>>(xsiTypeMap));

Our expected configuration should look something like this:

XMLParserConfiguration xmlConfig = new XMLParserConfiguration().withKeepStrings(false).WithXsiTypeMap(myXsiMap);

// use the config

After marking this private, also update the "clone" method (currently line 165). Be sure to follow the comment that is included in the "clone" method. If changing the constructor above to do the shallow clone/unmodifiable map wrapping, you can probably just update the clone to call this private constructor.

Lastly, be sure to create the new "with" method that will take the XSI:Type conversion map.

final boolean convertNilAttributeToNull, final Map<String, XMLXsiTypeConverter<?>> xsiTypeMap ) {
this.keepStrings = keepStrings;
this.cDataTagName = cDataTagName;
this.convertNilAttributeToNull = convertNilAttributeToNull;
this.xsiTypeMap = xsiTypeMap;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/json/XMLXsiTypeConverter.java
@@ -0,0 +1,5 @@
package org.json;
kumar529 marked this conversation as resolved.
Show resolved Hide resolved

public interface XMLXsiTypeConverter<T> {
T convert(String value);
}
44 changes: 42 additions & 2 deletions src/test/java/org/json/junit/XMLTest.java
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,42 @@ 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(false,
"content", false, xsiTypeMap));
Util.compareActualVsExpectedJsonObjects(actualJson,expectedJson);
}

}