Skip to content

Commit

Permalink
Merge pull request #857 from XIAYM-gh/master
Browse files Browse the repository at this point in the history
Implemented custom duplicate key handling (#840)
  • Loading branch information
stleary committed Mar 9, 2024
2 parents f9b5587 + 05b0897 commit 712859d
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 47 deletions.
56 changes: 40 additions & 16 deletions src/main/java/org/json/JSONObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,8 @@
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;

/**
Expand Down Expand Up @@ -205,6 +196,21 @@ public JSONObject(JSONObject jo, String ... names) {
* 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;
Expand Down Expand Up @@ -234,13 +240,14 @@ public JSONObject(JSONTokener x) throws JSONException {

if (key != null) {
// Check if key exists
if (this.opt(key) != null) {
// key already exists
boolean keyExists = this.opt(key) != null;
if (keyExists && !jsonParserConfiguration.isOverwriteDuplicateKey()) {
throw x.syntaxError("Duplicate key \"" + key + "\"");
}
// Only add value if non-null

Object value = x.nextValue();
if (value!=null) {
// Only add value if non-null
if (value != null) {
this.put(key, value);
}
}
Expand Down Expand Up @@ -296,7 +303,6 @@ public JSONObject(Map<?, ?> m, JSONParserConfiguration jsonParserConfiguration)

/**
* Construct a JSONObject from a map with recursion depth.
*
*/
private JSONObject(Map<?, ?> m, int recursionDepth, JSONParserConfiguration jsonParserConfiguration) {
if (recursionDepth > jsonParserConfiguration.getMaxNestingDepth()) {
Expand Down Expand Up @@ -427,7 +433,25 @@ public JSONObject(Object object, String ... names) {
* 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);
}

/**
Expand Down
84 changes: 69 additions & 15 deletions src/main/java/org/json/JSONParserConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,77 @@
* Configuration object for the JSON parser. The configuration is immutable.
*/
public class JSONParserConfiguration extends ParserConfiguration {
/**
* Used to indicate whether to overwrite duplicate key or not.
*/
private boolean overwriteDuplicateKey;

/**
* Configuration with the default values.
*/
public JSONParserConfiguration() {
super();
}
/**
* Configuration with the default values.
*/
public JSONParserConfiguration() {
this(false);
}

@Override
protected JSONParserConfiguration clone() {
return new JSONParserConfiguration();
}
/**
* Configure the parser with argument overwriteDuplicateKey.
*
* @param overwriteDuplicateKey Indicate whether to overwrite duplicate key or not.<br>
* If not, the JSONParser will throw a {@link JSONException}
* when meeting duplicate keys.
*/
public JSONParserConfiguration(boolean overwriteDuplicateKey) {
super();
this.overwriteDuplicateKey = overwriteDuplicateKey;
}

@SuppressWarnings("unchecked")
@Override
public JSONParserConfiguration withMaxNestingDepth(final int maxNestingDepth) {
return super.withMaxNestingDepth(maxNestingDepth);
}
@Override
protected JSONParserConfiguration clone() {
JSONParserConfiguration clone = new JSONParserConfiguration(overwriteDuplicateKey);
clone.maxNestingDepth = maxNestingDepth;
return clone;
}

/**
* Defines the maximum nesting depth that the parser will descend before throwing an exception
* when parsing a map into JSONObject or parsing a {@link java.util.Collection} instance into
* JSONArray. The default max nesting depth is 512, which means the parser will throw a JsonException
* if the maximum depth is reached.
*
* @param maxNestingDepth the maximum nesting depth allowed to the JSON parser
* @return The existing configuration will not be modified. A new configuration is returned.
*/
@SuppressWarnings("unchecked")
@Override
public JSONParserConfiguration withMaxNestingDepth(final int maxNestingDepth) {
JSONParserConfiguration clone = this.clone();
clone.maxNestingDepth = maxNestingDepth;

return clone;
}

/**
* Controls the parser's behavior when meeting duplicate keys.
* If set to false, the parser will throw a JSONException when meeting a duplicate key.
* Or the duplicate key's value will be overwritten.
*
* @param overwriteDuplicateKey defines should the parser overwrite duplicate keys.
* @return The existing configuration will not be modified. A new configuration is returned.
*/
public JSONParserConfiguration withOverwriteDuplicateKey(final boolean overwriteDuplicateKey) {
JSONParserConfiguration clone = this.clone();
clone.overwriteDuplicateKey = overwriteDuplicateKey;

return clone;
}

/**
* The parser's behavior when meeting duplicate keys, controls whether the parser should
* overwrite duplicate keys or not.
*
* @return The <code>overwriteDuplicateKey</code> configuration value.
*/
public boolean isOverwriteDuplicateKey() {
return this.overwriteDuplicateKey;
}
}
32 changes: 16 additions & 16 deletions src/main/java/org/json/ParserConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ public class ParserConfiguration {

/**
* Specifies if values should be kept as strings (<code>true</code>), or if
* they should try to be guessed into JSON values (numeric, boolean, string)
* they should try to be guessed into JSON values (numeric, boolean, string).
*/
protected boolean keepStrings;

/**
* The maximum nesting depth when parsing a document.
* The maximum nesting depth when parsing an object.
*/
protected int maxNestingDepth;

Expand Down Expand Up @@ -59,14 +59,14 @@ protected ParserConfiguration clone() {
// map should be cloned as well. If the values of the map are known to also
// be immutable, then a shallow clone of the map is acceptable.
return new ParserConfiguration(
this.keepStrings,
this.maxNestingDepth
this.keepStrings,
this.maxNestingDepth
);
}

/**
* When parsing the XML into JSONML, specifies if values should be kept as strings (<code>true</code>), or if
* they should try to be guessed into JSON values (numeric, boolean, string)
* they should try to be guessed into JSON values (numeric, boolean, string).
*
* @return The <code>keepStrings</code> configuration value.
*/
Expand All @@ -78,22 +78,21 @@ public boolean isKeepStrings() {
* When parsing the XML into JSONML, specifies if values 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>keepStrings</code> configuration option.
* @param <T> the type of the configuration object
*
* @param newVal new value to use for the <code>keepStrings</code> configuration option.
* @param <T> the type of the configuration object
* @return The existing configuration will not be modified. A new configuration is returned.
*/
@SuppressWarnings("unchecked")
public <T extends ParserConfiguration> T withKeepStrings(final boolean newVal) {
T newConfig = (T)this.clone();
T newConfig = (T) this.clone();
newConfig.keepStrings = newVal;
return newConfig;
}

/**
* The maximum nesting depth that the parser will descend before throwing an exception
* when parsing the XML into JSONML.
* when parsing an object (e.g. Map, Collection) into JSON-related objects.
*
* @return the maximum nesting depth set for this configuration
*/
public int getMaxNestingDepth() {
Expand All @@ -102,18 +101,19 @@ public int getMaxNestingDepth() {

/**
* Defines the maximum nesting depth that the parser will descend before throwing an exception
* when parsing the XML into JSONML. The default max nesting depth is 512, which means the parser
* will throw a JsonException if the maximum depth is reached.
* when parsing an object (e.g. Map, Collection) into JSON-related objects.
* The default max nesting depth is 512, which means the parser will throw a JsonException if
* the maximum depth is reached.
* Using any negative value as a parameter is equivalent to setting no limit to the nesting depth,
* which means the parses will go as deep as the maximum call stack size allows.
*
* @param maxNestingDepth the maximum nesting depth allowed to the XML parser
* @param <T> the type of the configuration object
*
* @param <T> the type of the configuration object
* @return The existing configuration will not be modified. A new configuration is returned.
*/
@SuppressWarnings("unchecked")
public <T extends ParserConfiguration> T withMaxNestingDepth(int maxNestingDepth) {
T newConfig = (T)this.clone();
T newConfig = (T) this.clone();

if (maxNestingDepth > UNDEFINED_MAXIMUM_NESTING_DEPTH) {
newConfig.maxNestingDepth = maxNestingDepth;
Expand Down
45 changes: 45 additions & 0 deletions src/test/java/org/json/junit/JSONParserConfigurationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.json.junit;

import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONParserConfiguration;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class JSONParserConfigurationTest {
private static final String TEST_SOURCE = "{\"key\": \"value1\", \"key\": \"value2\"}";

@Test(expected = JSONException.class)
public void testThrowException() {
new JSONObject(TEST_SOURCE);
}

@Test
public void testOverwrite() {
JSONObject jsonObject = new JSONObject(TEST_SOURCE, new JSONParserConfiguration(true));

assertEquals("duplicate key should be overwritten", "value2", jsonObject.getString("key"));
}

@Test
public void verifyDuplicateKeyThenMaxDepth() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
.withOverwriteDuplicateKey(true)
.withMaxNestingDepth(42);

assertEquals(42, jsonParserConfiguration.getMaxNestingDepth());
assertTrue(jsonParserConfiguration.isOverwriteDuplicateKey());
}

@Test
public void verifyMaxDepthThenDuplicateKey() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
.withMaxNestingDepth(42)
.withOverwriteDuplicateKey(true);

assertTrue(jsonParserConfiguration.isOverwriteDuplicateKey());
assertEquals(42, jsonParserConfiguration.getMaxNestingDepth());
}
}

0 comments on commit 712859d

Please sign in to comment.