Merge pull request #966 from marilynel/master

granular flags to control for keeping boolean or number values as strings
This commit is contained in:
Sean Leary 2025-04-04 07:40:55 -05:00 committed by GitHub
commit 2e9ad6ff5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 204 additions and 16 deletions

View File

@ -355,10 +355,20 @@ public class XML {
&& TYPE_ATTR.equals(string)) { && TYPE_ATTR.equals(string)) {
xmlXsiTypeConverter = config.getXsiTypeMap().get(token); xmlXsiTypeConverter = config.getXsiTypeMap().get(token);
} else if (!nilAttributeFound) { } else if (!nilAttributeFound) {
jsonObject.accumulate(string, Object obj = stringToValue((String) token);
config.isKeepStrings() if (obj instanceof Boolean) {
? ((String) token) jsonObject.accumulate(string,
: stringToValue((String) token)); config.isKeepBooleanAsString()
? ((String) token)
: obj);
} else if (obj instanceof Number) {
jsonObject.accumulate(string,
config.isKeepNumberAsString()
? ((String) token)
: obj);
} else {
jsonObject.accumulate(string, stringToValue((String) token));
}
} }
token = null; token = null;
} else { } else {
@ -407,8 +417,20 @@ public class XML {
jsonObject.accumulate(config.getcDataTagName(), jsonObject.accumulate(config.getcDataTagName(),
stringToValue(string, xmlXsiTypeConverter)); stringToValue(string, xmlXsiTypeConverter));
} else { } else {
jsonObject.accumulate(config.getcDataTagName(), Object obj = stringToValue((String) token);
config.isKeepStrings() ? string : stringToValue(string)); if (obj instanceof Boolean) {
jsonObject.accumulate(config.getcDataTagName(),
config.isKeepBooleanAsString()
? ((String) token)
: obj);
} else if (obj instanceof Number) {
jsonObject.accumulate(config.getcDataTagName(),
config.isKeepNumberAsString()
? ((String) token)
: obj);
} else {
jsonObject.accumulate(config.getcDataTagName(), stringToValue((String) token));
}
} }
} }
@ -688,6 +710,44 @@ public class XML {
return toJSONObject(reader, XMLParserConfiguration.ORIGINAL); return toJSONObject(reader, XMLParserConfiguration.ORIGINAL);
} }
/**
* Convert a well-formed (but not necessarily valid) XML into a
* JSONObject. Some information may be lost in this transformation because
* JSON is a data format and XML is a document format. XML uses elements,
* attributes, and content text, while JSON uses unordered collections of
* name/value pairs and arrays of values. JSON does not does not like to
* distinguish between elements and attributes. Sequences of similar
* elements are represented as JSONArrays. Content text may be placed in a
* "content" member. Comments, prologs, DTDs, and <pre>{@code
* &lt;[ [ ]]>}</pre>
* are ignored.
*
* All numbers are converted as strings, for 1, 01, 29.0 will not be coerced to
* numbers but will instead be the exact value as seen in the XML document depending
* on how flag is set.
* All booleans are converted as strings, for true, false will not be coerced to
* booleans but will instead be the exact value as seen in the XML document depending
* on how flag is set.
*
* @param reader The XML source reader.
* @param keepNumberAsString If true, then numeric values will not be coerced into
* numeric values and will instead be left as strings
* @param keepBooleanAsString If true, then boolean values will not be coerced into
* * numeric values and will instead be left as strings
* @return A JSONObject containing the structured data from the XML string.
* @throws JSONException Thrown if there is an errors while parsing the string
*/
public static JSONObject toJSONObject(Reader reader, boolean keepNumberAsString, boolean keepBooleanAsString) throws JSONException {
XMLParserConfiguration xmlParserConfiguration = new XMLParserConfiguration();
if(keepNumberAsString) {
xmlParserConfiguration = xmlParserConfiguration.withKeepNumberAsString(keepNumberAsString);
}
if(keepBooleanAsString) {
xmlParserConfiguration = xmlParserConfiguration.withKeepBooleanAsString(keepBooleanAsString);
}
return toJSONObject(reader, xmlParserConfiguration);
}
/** /**
* Convert a well-formed (but not necessarily valid) XML into a * Convert a well-formed (but not necessarily valid) XML into a
* JSONObject. Some information may be lost in this transformation because * JSONObject. Some information may be lost in this transformation because
@ -746,6 +806,38 @@ public class XML {
return toJSONObject(new StringReader(string), keepStrings); return toJSONObject(new StringReader(string), keepStrings);
} }
/**
* Convert a well-formed (but not necessarily valid) XML string into a
* JSONObject. Some information may be lost in this transformation because
* JSON is a data format and XML is a document format. XML uses elements,
* attributes, and content text, while JSON uses unordered collections of
* name/value pairs and arrays of values. JSON does not does not like to
* distinguish between elements and attributes. Sequences of similar
* elements are represented as JSONArrays. Content text may be placed in a
* "content" member. Comments, prologs, DTDs, and <pre>{@code
* &lt;[ [ ]]>}</pre>
* are ignored.
*
* All numbers are converted as strings, for 1, 01, 29.0 will not be coerced to
* numbers but will instead be the exact value as seen in the XML document depending
* on how flag is set.
* All booleans are converted as strings, for true, false will not be coerced to
* booleans but will instead be the exact value as seen in the XML document depending
* on how flag is set.
*
* @param string
* The source string.
* @param keepNumberAsString If true, then numeric values will not be coerced into
* numeric values and will instead be left as strings
* @param keepBooleanAsString If true, then boolean values will not be coerced into
* numeric values and will instead be left as strings
* @return A JSONObject containing the structured data from the XML string.
* @throws JSONException Thrown if there is an errors while parsing the string
*/
public static JSONObject toJSONObject(String string, boolean keepNumberAsString, boolean keepBooleanAsString) throws JSONException {
return toJSONObject(new StringReader(string), keepNumberAsString, keepBooleanAsString);
}
/** /**
* Convert a well-formed (but not necessarily valid) XML string into a * Convert a well-formed (but not necessarily valid) XML string into a
* JSONObject. Some information may be lost in this transformation because * JSONObject. Some information may be lost in this transformation because

View File

@ -22,6 +22,16 @@ public class XMLParserConfiguration extends ParserConfiguration {
*/ */
// public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512; // We could override // public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512; // We could override
/**
* Allow user to control how numbers are parsed
*/
private boolean keepNumberAsString;
/**
* Allow user to control how booleans are parsed
*/
private boolean keepBooleanAsString;
/** Original Configuration of the XML Parser. */ /** Original Configuration of the XML Parser. */
public static final XMLParserConfiguration ORIGINAL public static final XMLParserConfiguration ORIGINAL
= new XMLParserConfiguration(); = new XMLParserConfiguration();
@ -142,7 +152,9 @@ public class XMLParserConfiguration extends ParserConfiguration {
*/ */
@Deprecated @Deprecated
public XMLParserConfiguration (final boolean keepStrings, final String cDataTagName, final boolean convertNilAttributeToNull) { public XMLParserConfiguration (final boolean keepStrings, final String cDataTagName, final boolean convertNilAttributeToNull) {
super(keepStrings, DEFAULT_MAXIMUM_NESTING_DEPTH); super(false, DEFAULT_MAXIMUM_NESTING_DEPTH);
this.keepNumberAsString = keepStrings;
this.keepBooleanAsString = keepStrings;
this.cDataTagName = cDataTagName; this.cDataTagName = cDataTagName;
this.convertNilAttributeToNull = convertNilAttributeToNull; this.convertNilAttributeToNull = convertNilAttributeToNull;
} }
@ -163,8 +175,10 @@ public class XMLParserConfiguration extends ParserConfiguration {
*/ */
private XMLParserConfiguration (final boolean keepStrings, final String cDataTagName, private XMLParserConfiguration (final boolean keepStrings, final String cDataTagName,
final boolean convertNilAttributeToNull, final Map<String, XMLXsiTypeConverter<?>> xsiTypeMap, final Set<String> forceList, final boolean convertNilAttributeToNull, final Map<String, XMLXsiTypeConverter<?>> xsiTypeMap, final Set<String> forceList,
final int maxNestingDepth, final boolean closeEmptyTag) { final int maxNestingDepth, final boolean closeEmptyTag, final boolean keepNumberAsString, final boolean keepBooleanAsString) {
super(keepStrings, maxNestingDepth); super(false, maxNestingDepth);
this.keepNumberAsString = keepNumberAsString;
this.keepBooleanAsString = keepBooleanAsString;
this.cDataTagName = cDataTagName; this.cDataTagName = cDataTagName;
this.convertNilAttributeToNull = convertNilAttributeToNull; this.convertNilAttributeToNull = convertNilAttributeToNull;
this.xsiTypeMap = Collections.unmodifiableMap(xsiTypeMap); this.xsiTypeMap = Collections.unmodifiableMap(xsiTypeMap);
@ -189,7 +203,9 @@ public class XMLParserConfiguration extends ParserConfiguration {
this.xsiTypeMap, this.xsiTypeMap,
this.forceList, this.forceList,
this.maxNestingDepth, this.maxNestingDepth,
this.closeEmptyTag this.closeEmptyTag,
this.keepNumberAsString,
this.keepBooleanAsString
); );
config.shouldTrimWhiteSpace = this.shouldTrimWhiteSpace; config.shouldTrimWhiteSpace = this.shouldTrimWhiteSpace;
return config; return config;
@ -207,7 +223,40 @@ public class XMLParserConfiguration extends ParserConfiguration {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public XMLParserConfiguration withKeepStrings(final boolean newVal) { public XMLParserConfiguration withKeepStrings(final boolean newVal) {
return super.withKeepStrings(newVal); XMLParserConfiguration newConfig = this.clone();
newConfig.keepNumberAsString = newVal;
newConfig.keepBooleanAsString = newVal;
return newConfig;
}
/**
* When parsing the XML into JSON, specifies if numbers should be kept as strings (<code>1</code>), or if
* they should try to be guessed into JSON values (numeric, boolean, string)
*
* @param newVal
* new value to use for the <code>keepNumberAsString</code> configuration option.
*
* @return The existing configuration will not be modified. A new configuration is returned.
*/
public XMLParserConfiguration withKeepNumberAsString(final boolean newVal) {
XMLParserConfiguration newConfig = this.clone();
newConfig.keepNumberAsString = newVal;
return newConfig;
}
/**
* When parsing the XML into JSON, specifies if booleans should be kept as strings (<code>true</code>), or if
* they should try to be guessed into JSON values (numeric, boolean, string)
*
* @param newVal
* new value to use for the <code>withKeepBooleanAsString</code> configuration option.
*
* @return The existing configuration will not be modified. A new configuration is returned.
*/
public XMLParserConfiguration withKeepBooleanAsString(final boolean newVal) {
XMLParserConfiguration newConfig = this.clone();
newConfig.keepBooleanAsString = newVal;
return newConfig;
} }
/** /**
@ -221,6 +270,26 @@ public class XMLParserConfiguration extends ParserConfiguration {
return this.cDataTagName; return this.cDataTagName;
} }
/**
* When parsing the XML into JSONML, specifies if numbers should be kept as strings (<code>true</code>), or if
* they should try to be guessed into JSON values (numeric, boolean, string).
*
* @return The <code>keepStrings</code> configuration value.
*/
public boolean isKeepNumberAsString() {
return this.keepNumberAsString;
}
/**
* When parsing the XML into JSONML, specifies if booleans should be kept as strings (<code>true</code>), or if
* they should try to be guessed into JSON values (numeric, boolean, string).
*
* @return The <code>keepStrings</code> configuration value.
*/
public boolean isKeepBooleanAsString() {
return this.keepBooleanAsString;
}
/** /**
* The name of the key in a JSON Object that indicates a CDATA section. Historically this has * The name of the key in a JSON Object that indicates a CDATA section. Historically this has
* been the value "content" but can be changed. Use <code>null</code> to indicate no CDATA * been the value "content" but can be changed. Use <code>null</code> to indicate no CDATA

View File

@ -574,15 +574,18 @@ public class XMLConfigurationTest {
XMLParserConfiguration keepStringsAndCloseEmptyTag = keepStrings.withCloseEmptyTag(true); XMLParserConfiguration keepStringsAndCloseEmptyTag = keepStrings.withCloseEmptyTag(true);
XMLParserConfiguration keepDigits = keepStringsAndCloseEmptyTag.withKeepStrings(false); XMLParserConfiguration keepDigits = keepStringsAndCloseEmptyTag.withKeepStrings(false);
XMLParserConfiguration keepDigitsAndNoCloseEmptyTag = keepDigits.withCloseEmptyTag(false); XMLParserConfiguration keepDigitsAndNoCloseEmptyTag = keepDigits.withCloseEmptyTag(false);
assertTrue(keepStrings.isKeepStrings()); assertTrue(keepStrings.isKeepNumberAsString());
assertTrue(keepStrings.isKeepBooleanAsString());
assertFalse(keepStrings.isCloseEmptyTag()); assertFalse(keepStrings.isCloseEmptyTag());
assertTrue(keepStringsAndCloseEmptyTag.isKeepStrings()); assertTrue(keepStringsAndCloseEmptyTag.isKeepNumberAsString());
assertTrue(keepStringsAndCloseEmptyTag.isKeepBooleanAsString());
assertTrue(keepStringsAndCloseEmptyTag.isCloseEmptyTag()); assertTrue(keepStringsAndCloseEmptyTag.isCloseEmptyTag());
assertFalse(keepDigits.isKeepStrings()); assertFalse(keepDigits.isKeepNumberAsString());
assertFalse(keepDigits.isKeepBooleanAsString());
assertTrue(keepDigits.isCloseEmptyTag()); assertTrue(keepDigits.isCloseEmptyTag());
assertFalse(keepDigitsAndNoCloseEmptyTag.isKeepStrings()); assertFalse(keepDigitsAndNoCloseEmptyTag.isKeepNumberAsString());
assertFalse(keepDigitsAndNoCloseEmptyTag.isKeepBooleanAsString());
assertFalse(keepDigitsAndNoCloseEmptyTag.isCloseEmptyTag()); assertFalse(keepDigitsAndNoCloseEmptyTag.isCloseEmptyTag());
} }
/** /**
@ -767,6 +770,30 @@ public class XMLConfigurationTest {
Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected); Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected);
} }
/**
* JSON string lost leading zero and converted "True" to true.
*/
@Test
public void testToJSONArray_jsonOutput_withKeepNumberAsString() {
final String originalXml = "<root><id>01</id><id>1</id><id>00</id><id>0</id><item id=\"01\"/><title>True</title></root>";
final JSONObject expected = new JSONObject("{\"root\":{\"item\":{\"id\":\"01\"},\"id\":[\"01\",\"1\",\"00\",\"0\"],\"title\":true}}");
final JSONObject actualJsonOutput = XML.toJSONObject(originalXml,
new XMLParserConfiguration().withKeepNumberAsString(true));
Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected);
}
/**
* JSON string lost leading zero and converted "True" to true.
*/
@Test
public void testToJSONArray_jsonOutput_withKeepBooleanAsString() {
final String originalXml = "<root><id>01</id><id>1</id><id>00</id><id>0</id><item id=\"01\"/><title>True</title></root>";
final JSONObject expected = new JSONObject("{\"root\":{\"item\":{\"id\":\"01\"},\"id\":[\"01\",1,\"00\",0],\"title\":\"True\"}}");
final JSONObject actualJsonOutput = XML.toJSONObject(originalXml,
new XMLParserConfiguration().withKeepBooleanAsString(true));
Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected);
}
/** /**
* JSON string cannot be reverted to original xml. * JSON string cannot be reverted to original xml.
*/ */