diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index c81f15ff5..805a5c376 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -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. @@ -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 @@ -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: // @@ -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(); @@ -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() @@ -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) { @@ -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)}. * diff --git a/src/main/java/org/json/XMLParserConfiguration.java b/src/main/java/org/json/XMLParserConfiguration.java index cf5e10caa..b9e752c28 100644 --- a/src/main/java/org/json/XMLParserConfiguration.java +++ b/src/main/java/org/json/XMLParserConfiguration.java @@ -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 @@ -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> xsiTypeMap; + /** * Default parser configuration. Does not keep strings (tries to implicitly convert * values), and the CDATA Tag Name is "content". @@ -64,6 +74,7 @@ public XMLParserConfiguration () { this.keepStrings = false; this.cDataTagName = "content"; this.convertNilAttributeToNull = false; + this.xsiTypeMap = Collections.emptyMap(); } /** @@ -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 true to parse all values as string. + * false to try and convert XML string values into a JSON value. + * @param cDataTagName null to disable CDATA processing. Any other value + * to use that value as the JSONObject key name to process as CDATA. + * @param convertNilAttributeToNull true to parse values with attribute xsi:nil="true" as null. + * false to parse values with attribute xsi:nil="true" as {"xsi:nil":true}. + * @param xsiTypeMap new HashMap>() 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> xsiTypeMap ) { + this.keepStrings = keepStrings; + this.cDataTagName = cDataTagName; + this.convertNilAttributeToNull = convertNilAttributeToNull; + this.xsiTypeMap = Collections.unmodifiableMap(xsiTypeMap); + } + /** * Provides a new instance of the same configuration. */ @@ -143,7 +173,8 @@ protected XMLParserConfiguration clone() { return new XMLParserConfiguration( this.keepStrings, this.cDataTagName, - this.convertNilAttributeToNull + this.convertNilAttributeToNull, + this.xsiTypeMap ); } @@ -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>} to parse values with attribute + * xsi:type="integer" as integer, xsi:type="string" as string + * @return {@link #xsiTypeMap} unmodifiable configuration map. + */ + public Map> 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>} to parse values with attribute + * xsi:type="integer" as integer, xsi:type="string" as string + * @param xsiTypeMap {@code new HashMap>()} 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> xsiTypeMap) { + XMLParserConfiguration newConfig = this.clone(); + Map> cloneXsiTypeMap = new HashMap>(xsiTypeMap); + newConfig.xsiTypeMap = Collections.unmodifiableMap(cloneXsiTypeMap); + return newConfig; + } } diff --git a/src/main/java/org/json/XMLXsiTypeConverter.java b/src/main/java/org/json/XMLXsiTypeConverter.java new file mode 100644 index 000000000..0f8a8c332 --- /dev/null +++ b/src/main/java/org/json/XMLXsiTypeConverter.java @@ -0,0 +1,66 @@ +package org.json; +/* +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. + *
+ * XML Sample
+ * {@code
+ *      
+ *          12345
+ *          54321
+ *      
+ * }
+ * JSON Output
+ * {@code
+ *     {
+ *         "root" : {
+ *             "asString" : "12345",
+ *             "asInt": 54321
+ *         }
+ *     }
+ * }
+ *
+ * Usage
+ * {@code
+ *      Map> xsiTypeMap = new HashMap>();
+ *      xsiTypeMap.put("string", new XMLXsiTypeConverter() {
+ *          @Override public String convert(final String value) {
+ *              return value;
+ *          }
+ *      });
+ *      xsiTypeMap.put("integer", new XMLXsiTypeConverter() {
+ *          @Override public Integer convert(final String value) {
+ *              return Integer.valueOf(value);
+ *          }
+ *      });
+ * }
+ * 
+ * @author kumar529 + * @param return type of convert method + */ +public interface XMLXsiTypeConverter { + T convert(String value); +} diff --git a/src/test/java/org/json/junit/XMLTest.java b/src/test/java/org/json/junit/XMLTest.java index cf78350b4..62ee516b2 100644 --- a/src/test/java/org/json/junit/XMLTest.java +++ b/src/test/java/org/json/junit/XMLTest.java @@ -38,6 +38,8 @@ 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; @@ -45,6 +47,7 @@ of this software and associated documentation files (the "Software"), to deal 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; @@ -972,5 +975,97 @@ public void testIssue537CaseSensitiveHexUnEscapeDirect(){ assertEquals("Case insensitive Entity unescape", expectedStr, actualStr); } - -} \ No newline at end of file + + /** + * test passes when xsi:type="java.lang.String" not converting to string + */ + @Test + public void testToJsonWithTypeWhenTypeConversionDisabled() { + String originalXml = "1234"; + 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 = "1234" + + "1234"; + String expectedJsonString = "{\"root\":{\"id2\":1234,\"id1\":\"1234\"}}"; + JSONObject expectedJson = new JSONObject(expectedJsonString); + Map> xsiTypeMap = new HashMap>(); + xsiTypeMap.put("string", new XMLXsiTypeConverter() { + @Override public String convert(final String value) { + return value; + } + }); + xsiTypeMap.put("integer", new XMLXsiTypeConverter() { + @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 = "1234554321"; + String expectedJsonString = "{\"root\":{\"asString\":\"12345\",\"asInt\":54321}}"; + JSONObject expectedJson = new JSONObject(expectedJsonString); + Map> xsiTypeMap = new HashMap>(); + xsiTypeMap.put("string", new XMLXsiTypeConverter() { + @Override public String convert(final String value) { + return value; + } + }); + xsiTypeMap.put("integer", new XMLXsiTypeConverter() { + @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 = "1234554321"; + String expectedJsonString = "{\"root\":{\"asString\":\"12345\",\"asInt\":54321}}"; + JSONObject expectedJson = new JSONObject(expectedJsonString); + Map> xsiTypeMap = new HashMap>(); + xsiTypeMap.put("string", new XMLXsiTypeConverter() { + @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> xsiTypeMap = new HashMap>(); + XMLParserConfiguration config = new XMLParserConfiguration().withXsiTypeMap(xsiTypeMap); + xsiTypeMap.put("string", new XMLXsiTypeConverter() { + @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() { + @Override public Boolean convert(final String value) { + return Boolean.valueOf(value); + } + }); + fail("Expected to be unable to modify the config"); + } catch (Exception ignored) { } + } +}