Implemented custom duplicate key handling

- Supports: throw an exception (by default), ignore, overwrite & merge into a JSONArray
 - With tests, 4/4 passed.
This commit is contained in:
XIAYM-gh 2024-02-13 18:56:10 +08:00
parent 010e83b925
commit 10514e48cb
4 changed files with 191 additions and 34 deletions

View File

@ -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.<br/>
* 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
}

View File

@ -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<String> 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 <code>{</code>&nbsp;<small>(left
* brace)</small> and ending with <code>}</code>
* &nbsp;<small>(right brace)</small>.
* @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);
}
/**

View File

@ -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;
}
}

View File

@ -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"));
}
}