From 10514e48cb665a973e8c9b04db577fa6fdaddf07 Mon Sep 17 00:00:00 2001 From: XIAYM-gh Date: Tue, 13 Feb 2024 18:56:10 +0800 Subject: [PATCH] Implemented custom duplicate key handling - Supports: throw an exception (by default), ignore, overwrite & merge into a JSONArray - With tests, 4/4 passed. --- .../org/json/JSONDuplicateKeyStrategy.java | 28 ++++++ src/main/java/org/json/JSONObject.java | 96 +++++++++++++++---- .../org/json/JSONParserConfiguration.java | 54 ++++++++--- .../junit/JSONObjectDuplicateKeyTest.java | 47 +++++++++ 4 files changed, 191 insertions(+), 34 deletions(-) create mode 100644 src/main/java/org/json/JSONDuplicateKeyStrategy.java create mode 100644 src/test/java/org/json/junit/JSONObjectDuplicateKeyTest.java diff --git a/src/main/java/org/json/JSONDuplicateKeyStrategy.java b/src/main/java/org/json/JSONDuplicateKeyStrategy.java new file mode 100644 index 0000000..4652dbc --- /dev/null +++ b/src/main/java/org/json/JSONDuplicateKeyStrategy.java @@ -0,0 +1,28 @@ +package org.json; + +/** + * An enum class that is supposed to be used in {@link JSONParserConfiguration}, + * it dedicates which way should be used to handle duplicate keys. + */ +public enum JSONDuplicateKeyStrategy { + /** + * The default value. And this is the way it used to be in the previous versions.
+ * The JSONParser will throw an {@link JSONException} when meet duplicate key. + */ + THROW_EXCEPTION, + + /** + * The JSONParser will ignore duplicate keys and won't overwrite the value of the key. + */ + IGNORE, + + /** + * The JSONParser will overwrite the old value of the key. + */ + OVERWRITE, + + /** + * The JSONParser will try to merge the values of the duplicate key into a {@link JSONArray}. + */ + MERGE_INTO_ARRAY +} diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 039f136..e7d5cd5 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -15,17 +15,8 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; -import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.Locale; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.ResourceBundle; -import java.util.Set; import java.util.regex.Pattern; import static org.json.NumberConversionUtil.potentialNumber; @@ -203,9 +194,28 @@ public class JSONObject { * duplicated key. */ public JSONObject(JSONTokener x) throws JSONException { + this(x, new JSONParserConfiguration()); + } + + /** + * Construct a JSONObject from a JSONTokener with custom json parse configurations. + * + * @param x + * A JSONTokener object containing the source string. + * @param jsonParserConfiguration + * Variable to pass parser custom configuration for json parsing. + * @throws JSONException + * If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException { this(); char c; String key; + JSONDuplicateKeyStrategy duplicateKeyStrategy = jsonParserConfiguration.getDuplicateKeyStrategy(); + + // A list to store merged keys + List mergedKeys = null; if (x.nextClean() != '{') { throw x.syntaxError("A JSONObject text must begin with '{'"); @@ -232,14 +242,45 @@ public class JSONObject { if (key != null) { // Check if key exists - if (this.opt(key) != null) { - // key already exists - throw x.syntaxError("Duplicate key \"" + key + "\""); + boolean keyExists = this.opt(key) != null; + // Read value early to make the tokener work well + Object value = null; + if (!keyExists || duplicateKeyStrategy != JSONDuplicateKeyStrategy.THROW_EXCEPTION) { + value = x.nextValue(); } - // Only add value if non-null - Object value = x.nextValue(); - if (value!=null) { - this.put(key, value); + + if (keyExists) { + switch (duplicateKeyStrategy) { + case THROW_EXCEPTION: + throw x.syntaxError("Duplicate key \"" + key + "\""); + + case MERGE_INTO_ARRAY: + if (mergedKeys == null) { + mergedKeys = new ArrayList<>(); + } + + Object current = this.get(key); + if (current instanceof JSONArray && mergedKeys.contains(key)) { + ((JSONArray) current).put(value); + break; + } + + JSONArray merged = new JSONArray(); + merged.put(current); + merged.put(value); + this.put(key, merged); + mergedKeys.add(key); + break; + } + + // == IGNORE, ignored :) + } + + if (!keyExists || duplicateKeyStrategy == JSONDuplicateKeyStrategy.OVERWRITE) { + // Only add value if non-null + if (value != null) { + this.put(key, value); + } } } @@ -294,7 +335,6 @@ public class JSONObject { /** * Construct a JSONObject from a map with recursion depth. - * */ private JSONObject(Map m, int recursionDepth, JSONParserConfiguration jsonParserConfiguration) { if (recursionDepth > jsonParserConfiguration.getMaxNestingDepth()) { @@ -426,7 +466,25 @@ public class JSONObject { * duplicated key. */ public JSONObject(String source) throws JSONException { - this(new JSONTokener(source)); + this(source, new JSONParserConfiguration()); + } + + /** + * Construct a JSONObject from a source JSON text string with custom json parse configurations. + * This is the most commonly used JSONObject constructor. + * + * @param source + * A string beginning with { (left + * brace) and ending with } + *  (right brace). + * @param jsonParserConfiguration + * Variable to pass parser custom configuration for json parsing. + * @exception JSONException + * If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(String source, JSONParserConfiguration jsonParserConfiguration) throws JSONException { + this(new JSONTokener(source), jsonParserConfiguration); } /** diff --git a/src/main/java/org/json/JSONParserConfiguration.java b/src/main/java/org/json/JSONParserConfiguration.java index f95e244..f1ea2b2 100644 --- a/src/main/java/org/json/JSONParserConfiguration.java +++ b/src/main/java/org/json/JSONParserConfiguration.java @@ -4,23 +4,47 @@ package org.json; * Configuration object for the JSON parser. The configuration is immutable. */ public class JSONParserConfiguration extends ParserConfiguration { + /** + * The way should be used to handle duplicate keys. + */ + private JSONDuplicateKeyStrategy duplicateKeyStrategy; - /** - * Configuration with the default values. - */ - public JSONParserConfiguration() { - super(); - } + /** + * Configuration with the default values. + */ + public JSONParserConfiguration() { + this(JSONDuplicateKeyStrategy.THROW_EXCEPTION); + } - @Override - protected JSONParserConfiguration clone() { - return new JSONParserConfiguration(); - } + /** + * Configure the parser with {@link JSONDuplicateKeyStrategy}. + * + * @param duplicateKeyStrategy Indicate which way should be used to handle duplicate keys. + */ + public JSONParserConfiguration(JSONDuplicateKeyStrategy duplicateKeyStrategy) { + super(); + this.duplicateKeyStrategy = duplicateKeyStrategy; + } - @SuppressWarnings("unchecked") - @Override - public JSONParserConfiguration withMaxNestingDepth(final int maxNestingDepth) { - return super.withMaxNestingDepth(maxNestingDepth); - } + @Override + protected JSONParserConfiguration clone() { + return new JSONParserConfiguration(); + } + @SuppressWarnings("unchecked") + @Override + public JSONParserConfiguration withMaxNestingDepth(final int maxNestingDepth) { + return super.withMaxNestingDepth(maxNestingDepth); + } + + public JSONParserConfiguration withDuplicateKeyStrategy(final JSONDuplicateKeyStrategy duplicateKeyStrategy) { + JSONParserConfiguration newConfig = this.clone(); + newConfig.duplicateKeyStrategy = duplicateKeyStrategy; + + return newConfig; + } + + public JSONDuplicateKeyStrategy getDuplicateKeyStrategy() { + return this.duplicateKeyStrategy; + } } diff --git a/src/test/java/org/json/junit/JSONObjectDuplicateKeyTest.java b/src/test/java/org/json/junit/JSONObjectDuplicateKeyTest.java new file mode 100644 index 0000000..73dc70b --- /dev/null +++ b/src/test/java/org/json/junit/JSONObjectDuplicateKeyTest.java @@ -0,0 +1,47 @@ +package org.json.junit; + +import org.json.*; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class JSONObjectDuplicateKeyTest { + private static final String TEST_SOURCE = "{\"key\": \"value1\", \"key\": \"value2\", \"key\": \"value3\"}"; + + @Test(expected = JSONException.class) + public void testThrowException() { + new JSONObject(TEST_SOURCE); + } + + @Test + public void testIgnore() { + JSONObject jsonObject = new JSONObject(TEST_SOURCE, new JSONParserConfiguration( + JSONDuplicateKeyStrategy.IGNORE + )); + + assertEquals("duplicate key shouldn't be overwritten", "value1", jsonObject.getString("key")); + } + + @Test + public void testOverwrite() { + JSONObject jsonObject = new JSONObject(TEST_SOURCE, new JSONParserConfiguration( + JSONDuplicateKeyStrategy.OVERWRITE + )); + + assertEquals("duplicate key should be overwritten", "value3", jsonObject.getString("key")); + } + + @Test + public void testMergeIntoArray() { + JSONObject jsonObject = new JSONObject(TEST_SOURCE, new JSONParserConfiguration( + JSONDuplicateKeyStrategy.MERGE_INTO_ARRAY + )); + + JSONArray jsonArray; + assertTrue("duplicate key should be merged into JSONArray", jsonObject.get("key") instanceof JSONArray + && (jsonArray = jsonObject.getJSONArray("key")).length() == 3 + && jsonArray.getString(0).equals("value1") && jsonArray.getString(1).equals("value2") + && jsonArray.getString(2).equals("value3")); + } +}