Implement JSONObject#append.

It provides better semantics that accumulate. In particular,
the type of the mapping does not depend on the number of calls
to the function.

Change-Id: Ib8f9d229d8de72d57b25ff9d69c69a61215c2fd7
diff --git a/json/src/main/java/org/json/JSONArray.java b/json/src/main/java/org/json/JSONArray.java
index f6801aa..9b5f2b7 100644
--- a/json/src/main/java/org/json/JSONArray.java
+++ b/json/src/main/java/org/json/JSONArray.java
@@ -187,6 +187,17 @@
     }
 
     /**
+     * Same as {@link #put}, with added validity checks.
+     */
+    void checkedPut(Object value) throws JSONException {
+        if (value instanceof Number) {
+            JSON.checkDouble(((Number) value).doubleValue());
+        }
+
+        put(value);
+    }
+
+    /**
      * Sets the value at {@code index} to {@code value}, null padding this array
      * to the required length if necessary. If a value already exists at {@code
      * index}, it will be replaced.
diff --git a/json/src/main/java/org/json/JSONObject.java b/json/src/main/java/org/json/JSONObject.java
index 1f474b8..952fb85 100644
--- a/json/src/main/java/org/json/JSONObject.java
+++ b/json/src/main/java/org/json/JSONObject.java
@@ -283,6 +283,12 @@
      * mapped to {@code name}. In aggregate, this allows values to be added to a
      * mapping one at a time.
      *
+     * <p> Note that {@link #append(String, Object)} provides better semantics.
+     * In particular, the mapping for {@code name} will <b>always</b> be a
+     * {@link JSONArray}. Using {@code accumulate} will result in either a
+     * {@link JSONArray} or a mapping whose type is the type of {@code value}
+     * depending on the number of calls to it.
+     *
      * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
      *     Integer, Long, Double, {@link #NULL} or null. May not be {@link
      *     Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
@@ -293,23 +299,48 @@
             return put(name, value);
         }
 
-        // check in accumulate, since array.put(Object) doesn't do any checking
-        if (value instanceof Number) {
-            JSON.checkDouble(((Number) value).doubleValue());
-        }
-
         if (current instanceof JSONArray) {
             JSONArray array = (JSONArray) current;
-            array.put(value);
+            array.checkedPut(value);
         } else {
             JSONArray array = new JSONArray();
-            array.put(current);
-            array.put(value);
+            array.checkedPut(current);
+            array.checkedPut(value);
             nameValuePairs.put(name, array);
         }
         return this;
     }
 
+    /**
+     * Appends values to the array mapped to {@code name}. A new {@link JSONArray}
+     * mapping for {@code name} will be inserted if no mapping exists. If the existing
+     * mapping for {@code name} is not a {@link JSONArray}, a {@link JSONException}
+     * will be thrown.
+     *
+     * @throws JSONException if {@code name} is {@code null} or if the mapping for
+     *         {@code name} is non-null and is not a {@link JSONArray}.
+     *
+     * @hide
+     */
+    public JSONObject append(String name, Object value) throws JSONException {
+        Object current = nameValuePairs.get(checkName(name));
+
+        final JSONArray array;
+        if (current instanceof JSONArray) {
+            array = (JSONArray) current;
+        } else if (current == null) {
+            JSONArray newArray = new JSONArray();
+            nameValuePairs.put(name, newArray);
+            array = newArray;
+        } else {
+            throw new JSONException("Key " + name + " is not a JSONArray");
+        }
+
+        array.checkedPut(value);
+
+        return this;
+    }
+
     String checkName(String name) throws JSONException {
         if (name == null) {
             throw new JSONException("Names must be non-null");
diff --git a/json/src/test/java/org/json/JSONObjectTest.java b/json/src/test/java/org/json/JSONObjectTest.java
index d0a1d2e..e89db94 100644
--- a/json/src/test/java/org/json/JSONObjectTest.java
+++ b/json/src/test/java/org/json/JSONObjectTest.java
@@ -994,4 +994,39 @@
         map.put("y", list);
         assertEquals("{\"x\":\"l\",\"y\":[\"a\",[]]}", new JSONObject(map).toString());
     }
+
+    public void testAppendExistingInvalidKey() throws JSONException {
+        JSONObject object = new JSONObject();
+        object.put("foo", 5);
+        try {
+            object.append("foo", 6);
+            fail();
+        } catch (JSONException expected) {
+        }
+    }
+
+    public void testAppendExistingArray() throws JSONException {
+        JSONArray array = new JSONArray();
+        JSONObject object = new JSONObject();
+        object.put("foo", array);
+        object.append("foo", 5);
+        assertEquals("[5]", array.toString());
+    }
+
+    public void testAppendPutArray() throws JSONException {
+        JSONObject object = new JSONObject();
+        object.append("foo", 5);
+        assertEquals("{\"foo\":[5]}", object.toString());
+        object.append("foo", new JSONArray());
+        assertEquals("{\"foo\":[5,[]]}", object.toString());
+    }
+
+    public void testAppendNull() {
+        JSONObject object = new JSONObject();
+        try {
+            object.append(null, 5);
+            fail();
+        } catch (JSONException e) {
+        }
+    }
 }