diff --git a/JSONArray.java b/JSONArray.java index a402d67..8775a19 100644 --- a/JSONArray.java +++ b/JSONArray.java @@ -1230,7 +1230,7 @@ public class JSONArray implements Iterable { * Queries and returns a value from this object using {@code jsonPointer}, or * returns null if the query fails due to a missing key. * - * @param The JSON pointer + * @param jsonPointer The JSON pointer * @return the queried value or {@code null} * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax */ @@ -1323,8 +1323,9 @@ public class JSONArray implements Iterable { * whitespace is added. If it is not possible to produce a syntactically * correct JSON text then null will be returned instead. This could occur if * the array contains an invalid number. - *

+ *

* Warning: This method assumes that the data structure is acyclical. + * * * @return a printable, displayable, transmittable representation of the * array. @@ -1339,9 +1340,24 @@ public class JSONArray implements Iterable { } /** - * Make a pretty-printed JSON text of this JSONArray. Warning: This method - * assumes that the data structure is acyclical. - * + * Make a pretty-printed JSON text of this JSONArray. + * + *

If indentFactor > 0 and the {@link JSONArray} has only + * one element, then the array will be output on a single line: + *

{@code [1]}
+ * + *

If an array has 2 or more elements, then it will be output across + * multiple lines:

{@code
+     * [
+     * 1,
+     * "value 2",
+     * 3
+     * ]
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * * @param indentFactor * The number of spaces to add to each level of indentation. * @return a printable, displayable, transmittable representation of the @@ -1360,8 +1376,9 @@ public class JSONArray implements Iterable { /** * Write the contents of the JSONArray as JSON text to a writer. For * compactness, no whitespace is added. - *

+ *

* Warning: This method assumes that the data structure is acyclical. + * * * @return The writer. * @throws JSONException @@ -1371,10 +1388,23 @@ public class JSONArray implements Iterable { } /** - * Write the contents of the JSONArray as JSON text to a writer. For - * compactness, no whitespace is added. - *

+ * Write the contents of the JSONArray as JSON text to a writer. + * + *

If indentFactor > 0 and the {@link JSONArray} has only + * one element, then the array will be output on a single line: + *

{@code [1]}
+ * + *

If an array has 2 or more elements, then it will be output across + * multiple lines:

{@code
+     * [
+     * 1,
+     * "value 2",
+     * 3
+     * ]
+     * }
+ *

* Warning: This method assumes that the data structure is acyclical. + * * * @param writer * Writes the serialized JSON diff --git a/JSONML.java b/JSONML.java index 2dcbd25..acec7b8 100644 --- a/JSONML.java +++ b/JSONML.java @@ -172,7 +172,7 @@ public class JSONML { if (!(token instanceof String)) { throw x.syntaxError("Missing value"); } - newjo.accumulate(attribute, keepStrings ? XML.unescape((String)token) :XML.stringToValue((String)token)); + newjo.accumulate(attribute, keepStrings ? ((String)token) :XML.stringToValue((String)token)); token = null; } else { newjo.accumulate(attribute, ""); diff --git a/JSONObject.java b/JSONObject.java index 89c94f4..54efa6e 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -1,5 +1,7 @@ package org.json; +import java.io.Closeable; + /* Copyright (c) 2002 JSON.org @@ -28,6 +30,7 @@ import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigDecimal; @@ -228,7 +231,21 @@ public class JSONObject { if (c != ':') { throw x.syntaxError("Expected a ':' after a key"); } - this.putOnce(key, x.nextValue()); + + // Use syntaxError(..) to include error location + + if (key != null) { + // Check if key exists + if (this.opt(key) != null) { + // key already exists + throw x.syntaxError("Duplicate key \"" + key + "\""); + } + // Only add value if non-null + Object value = x.nextValue(); + if (value!=null) { + this.put(key, value); + } + } // Pairs are separated by ','. @@ -276,16 +293,19 @@ public class JSONObject { * "is" followed by an uppercase letter, the method is invoked, * and a key and the value returned from the getter method are put into the * new JSONObject. - * + *

* The key is formed by removing the "get" or "is" * prefix. If the second remaining character is not upper case, then the * first character is converted to lower case. - * + *

* For example, if an object has a method named "getName", and * if the result of calling object.getName() is * "Larry Fine", then the JSONObject will contain * "name": "Larry Fine". - * + *

+ * Methods that return void as well as static + * methods are ignored. + * * @param bean * An object that has getter methods that should be used to make * a JSONObject. @@ -1388,6 +1408,15 @@ public class JSONObject { return NULL.equals(object) ? defaultValue : object.toString(); } + /** + * Populates the internal map of the JSONObject with the bean properties. + * The bean can not be recursive. + * + * @see JSONObject#JSONObject(Object) + * + * @param bean + * the bean + */ private void populateMap(Object bean) { Class klass = bean.getClass(); @@ -1397,39 +1426,52 @@ public class JSONObject { Method[] methods = includeSuperClass ? klass.getMethods() : klass .getDeclaredMethods(); - for (int i = 0; i < methods.length; i += 1) { - try { - Method method = methods[i]; - if (Modifier.isPublic(method.getModifiers())) { - String name = method.getName(); - String key = ""; - if (name.startsWith("get")) { - if ("getClass".equals(name) - || "getDeclaringClass".equals(name)) { - key = ""; - } else { - key = name.substring(3); - } - } else if (name.startsWith("is")) { - key = name.substring(2); + for (final Method method : methods) { + final int modifiers = method.getModifiers(); + if (Modifier.isPublic(modifiers) + && !Modifier.isStatic(modifiers) + && method.getParameterTypes().length == 0 + && !method.isBridge() + && method.getReturnType() != Void.TYPE ) { + final String name = method.getName(); + String key; + if (name.startsWith("get")) { + if ("getClass".equals(name) || "getDeclaringClass".equals(name)) { + continue; + } + key = name.substring(3); + } else if (name.startsWith("is")) { + key = name.substring(2); + } else { + continue; + } + if (key.length() > 0 + && Character.isUpperCase(key.charAt(0))) { + if (key.length() == 1) { + key = key.toLowerCase(Locale.ROOT); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase(Locale.ROOT) + + key.substring(1); } - if (key.length() > 0 - && Character.isUpperCase(key.charAt(0)) - && method.getParameterTypes().length == 0) { - if (key.length() == 1) { - key = key.toLowerCase(Locale.ROOT); - } else if (!Character.isUpperCase(key.charAt(1))) { - key = key.substring(0, 1).toLowerCase(Locale.ROOT) - + key.substring(1); - } - Object result = method.invoke(bean, (Object[]) null); + try { + final Object result = method.invoke(bean); if (result != null) { this.map.put(key, wrap(result)); + // we don't use the result anywhere outside of wrap + // if it's a resource we should be sure to close it after calling toString + if(result instanceof Closeable) { + try { + ((Closeable)result).close(); + } catch (IOException ignore) { + } + } } + } catch (IllegalAccessException ignore) { + } catch (IllegalArgumentException ignore) { + } catch (InvocationTargetException ignore) { } } - } catch (Exception ignore) { } } } @@ -1676,7 +1718,7 @@ public class JSONObject { * Queries and returns a value from this object using {@code jsonPointer}, or * returns null if the query fails due to a missing key. * - * @param The JSON pointer + * @param jsonPointer The JSON pointer * @return the queried value or {@code null} * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax */ @@ -2004,9 +2046,10 @@ public class JSONObject { * Make a JSON text of this JSONObject. For compactness, no whitespace is * added. If this would not result in a syntactically correct JSON text, * then null will be returned instead. - *

+ *

* Warning: This method assumes that the data structure is acyclical. - * + * + * * @return a printable, displayable, portable, transmittable representation * of the object, beginning with { (left * brace) and ending with } (right @@ -2023,8 +2066,20 @@ public class JSONObject { /** * Make a pretty-printed JSON text of this JSONObject. - *

+ * + *

If indentFactor > 0 and the {@link JSONObject} + * has only one key, then the object will be output on a single line: + *

{@code {"key": 1}}
+ * + *

If an object has 2 or more keys, then it will be output across + * multiple lines:

{
+     *  "key1": 1,
+     *  "key2": "value 2",
+     *  "key3": 3
+     * }
+ *

* Warning: This method assumes that the data structure is acyclical. + * * * @param indentFactor * The number of spaces to add to each level of indentation. @@ -2130,9 +2185,10 @@ public class JSONObject { /** * Write the contents of the JSONObject as JSON text to a writer. For * compactness, no whitespace is added. - *

+ *

* Warning: This method assumes that the data structure is acyclical. - * + * + * * @return The writer. * @throws JSONException */ @@ -2196,8 +2252,20 @@ public class JSONObject { /** * Write the contents of the JSONObject as JSON text to a writer. - *

+ * + *

If indentFactor > 0 and the {@link JSONObject} + * has only one key, then the object will be output on a single line: + *

{@code {"key": 1}}
+ * + *

If an object has 2 or more keys, then it will be output across + * multiple lines:

{
+     *  "key1": 1,
+     *  "key2": "value 2",
+     *  "key3": 3
+     * }
+ *

* Warning: This method assumes that the data structure is acyclical. + * * * @param writer * Writes the serialized JSON diff --git a/README b/README index 2fbae18..9783046 100644 --- a/README +++ b/README @@ -89,6 +89,8 @@ invalid number formats (1.2e6.3) will cause errors as such documents can not be reliably. Release history: +20171018 Checkpoint for recent commits. + 20170516 Roll up recent commits. 20160810 Revert code that was breaking opt*() methods. diff --git a/XML.java b/XML.java index b2cff20..08666f7 100644 --- a/XML.java +++ b/XML.java @@ -140,7 +140,7 @@ public class XML { if (mustEscape(cp)) { sb.append("&#x"); sb.append(Integer.toHexString(cp)); - sb.append(";"); + sb.append(';'); } else { sb.appendCodePoint(cp); } @@ -190,31 +190,7 @@ public class XML { final int semic = string.indexOf(';', i); if (semic > i) { final String entity = string.substring(i + 1, semic); - if (entity.charAt(0) == '#') { - int cp; - if (entity.charAt(1) == 'x') { - // hex encoded unicode - cp = Integer.parseInt(entity.substring(2), 16); - } else { - // decimal encoded unicode - cp = Integer.parseInt(entity.substring(1)); - } - sb.appendCodePoint(cp); - } else { - if ("quot".equalsIgnoreCase(entity)) { - sb.append('"'); - } else if ("amp".equalsIgnoreCase(entity)) { - sb.append('&'); - } else if ("apos".equalsIgnoreCase(entity)) { - sb.append('\''); - } else if ("lt".equalsIgnoreCase(entity)) { - sb.append('<'); - } else if ("gt".equalsIgnoreCase(entity)) { - sb.append('>'); - } else {// unsupported xml entity. leave encoded - sb.append('&').append(entity).append(';'); - } - } + sb.append(XMLTokener.unescapeEntity(entity)); // skip past the entity we just parsed. i += entity.length() + 1; } else { @@ -363,7 +339,7 @@ public class XML { throw x.syntaxError("Missing value"); } jsonobject.accumulate(string, - keepStrings ? unescape((String)token) : stringToValue((String) token)); + keepStrings ? ((String)token) : stringToValue((String) token)); token = null; } else { jsonobject.accumulate(string, ""); @@ -395,7 +371,7 @@ public class XML { string = (String) token; if (string.length() > 0) { jsonobject.accumulate("content", - keepStrings ? unescape(string) : stringToValue(string)); + keepStrings ? string : stringToValue(string)); } } else if (token == LT) { @@ -422,14 +398,15 @@ public class XML { } /** - * This method is the same as {@link JSONObject.stringToValue(String)} - * except that this also tries to unescape String values. + * This method is the same as {@link JSONObject#stringToValue(String)}. * * @param string String to convert * @return JSON value of this string or the string */ + // To maintain compatibility with the Android API, this method is a direct copy of + // the one in JSONObject. Changes made here should be reflected there. public static Object stringToValue(String string) { - if (string.equals("")) { + if (string.equals("")) { return string; } if (string.equalsIgnoreCase("true")) { @@ -470,8 +447,7 @@ public class XML { } catch (Exception ignore) { } } - - return unescape(string); + return string; } /** diff --git a/XMLTokener.java b/XMLTokener.java index 2ff3aff..1da8b84 100644 --- a/XMLTokener.java +++ b/XMLTokener.java @@ -138,8 +138,37 @@ public class XMLTokener extends JSONTokener { } } String string = sb.toString(); - Object object = entity.get(string); - return object != null ? object : ampersand + string + ";"; + return unescapeEntity(string); + } + + /** + * Unescapes an XML entity encoding; + * @param e entity (only the actual entity value, not the preceding & or ending ; + * @return + */ + static String unescapeEntity(String e) { + // validate + if (e == null || e.isEmpty()) { + return ""; + } + // if our entity is an encoded unicode point, parse it. + if (e.charAt(0) == '#') { + int cp; + if (e.charAt(1) == 'x') { + // hex encoded unicode + cp = Integer.parseInt(e.substring(2), 16); + } else { + // decimal encoded unicode + cp = Integer.parseInt(e.substring(1)); + } + return new String(new int[] {cp},0,1); + } + Character knownEntity = entity.get(e); + if(knownEntity==null) { + // we don't know the entity so keep it encoded + return '&' + e + ';'; + } + return knownEntity.toString(); }