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 3 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.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
Original file line number Diff line number Diff line change
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
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>
* <h1>XML Sample</h1>
* {@code
* <root>
* <asString xsi:type="string">12345</asString>
* <asInt xsi:type="integer">54321</asInt>
* </root>
* }
* <h1>JSON Output</h1>
* {@code
* {
* "root" : {
* "asString" : "12345",
* "asInt": 54321
* }
* }
* }
*
* <h1>Usage</h1>
* {@code
* 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);
* }
* });
* }
* </pre>
* @author kumar529
* @param <T>
*/
public interface XMLXsiTypeConverter<T> {
T convert(String value);
}
66 changes: 64 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,64 @@ 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);
}

@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(false,
"content", false, xsiTypeMap));
Util.compareActualVsExpectedJsonObjects(actualJson,expectedJson);
}

}