Issue #240 - added support for "reject duplicate keys" config option (#241)

added option: REJECT_DUPLICATE_KEYS

Signed-off-by: John T.E. Timm <johntimm@us.ibm.com>
diff --git a/impl/src/main/java/org/glassfish/json/JsonBuilderFactoryImpl.java b/impl/src/main/java/org/glassfish/json/JsonBuilderFactoryImpl.java
index 3498e9a..7fde7a3 100644
--- a/impl/src/main/java/org/glassfish/json/JsonBuilderFactoryImpl.java
+++ b/impl/src/main/java/org/glassfish/json/JsonBuilderFactoryImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -33,25 +33,27 @@
 class JsonBuilderFactoryImpl implements JsonBuilderFactory {
     private final Map<String, ?> config;
     private final BufferPool bufferPool;
+    private final boolean rejectDuplicateKeys;
 
-    JsonBuilderFactoryImpl(BufferPool bufferPool) {
+    JsonBuilderFactoryImpl(BufferPool bufferPool, boolean rejectDuplicateKeys) {
         this.config = Collections.emptyMap();
         this.bufferPool = bufferPool;
+        this.rejectDuplicateKeys = rejectDuplicateKeys;
     }
 
     @Override
     public JsonObjectBuilder createObjectBuilder() {
-        return new JsonObjectBuilderImpl(bufferPool);
+        return new JsonObjectBuilderImpl(bufferPool, rejectDuplicateKeys);
     }
  
     @Override
     public JsonObjectBuilder createObjectBuilder(JsonObject object) {
-        return new JsonObjectBuilderImpl(object, bufferPool);
+        return new JsonObjectBuilderImpl(object, bufferPool, rejectDuplicateKeys);
     }
 
     @Override
     public JsonObjectBuilder createObjectBuilder(Map<String, Object> object) {
-        return new JsonObjectBuilderImpl(object, bufferPool);
+        return new JsonObjectBuilderImpl(object, bufferPool, rejectDuplicateKeys);
     }
 
     @Override
diff --git a/impl/src/main/java/org/glassfish/json/JsonMessages.java b/impl/src/main/java/org/glassfish/json/JsonMessages.java
index 219b507..aeec4e4 100644
--- a/impl/src/main/java/org/glassfish/json/JsonMessages.java
+++ b/impl/src/main/java/org/glassfish/json/JsonMessages.java
@@ -116,6 +116,10 @@
     static String PARSER_INPUT_ENC_DETECT_IOERR() {
         return localize("parser.input.enc.detect.ioerr");
     }
+    
+    static String DUPLICATE_KEY(String name) {
+        return localize("parser.duplicate.key", name);
+    }
 
     // generator messages
     static String GENERATOR_FLUSH_IO_ERR() {
diff --git a/impl/src/main/java/org/glassfish/json/JsonObjectBuilderImpl.java b/impl/src/main/java/org/glassfish/json/JsonObjectBuilderImpl.java
index 9d20e4a..1104055 100644
--- a/impl/src/main/java/org/glassfish/json/JsonObjectBuilderImpl.java
+++ b/impl/src/main/java/org/glassfish/json/JsonObjectBuilderImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -18,7 +18,6 @@
 
 import org.glassfish.json.api.BufferPool;
 
-import jakarta.json.JsonArrayBuilder;
 import jakarta.json.*;
 import java.io.StringWriter;
 import java.math.BigDecimal;
@@ -33,23 +32,46 @@
  */
 class JsonObjectBuilderImpl implements JsonObjectBuilder {
 
-    private Map<String, JsonValue> valueMap;
+    protected Map<String, JsonValue> valueMap;
     private final BufferPool bufferPool;
+    private final boolean rejectDuplicateKeys;
 
     JsonObjectBuilderImpl(BufferPool bufferPool) {
         this.bufferPool = bufferPool;
+        rejectDuplicateKeys = false;
+    }
+    
+    JsonObjectBuilderImpl(BufferPool bufferPool, boolean rejectDuplicateKeys) {
+        this.bufferPool = bufferPool;
+        this.rejectDuplicateKeys = rejectDuplicateKeys;
     }
 
     JsonObjectBuilderImpl(JsonObject object, BufferPool bufferPool) {
         this.bufferPool = bufferPool;
         valueMap = new LinkedHashMap<>();
         valueMap.putAll(object);
+        rejectDuplicateKeys = false;
+    }
+    
+    JsonObjectBuilderImpl(JsonObject object, BufferPool bufferPool, boolean rejectDuplicateKeys) {
+        this.bufferPool = bufferPool;
+        valueMap = new LinkedHashMap<>();
+        valueMap.putAll(object);
+        this.rejectDuplicateKeys = rejectDuplicateKeys;
     }
 
     JsonObjectBuilderImpl(Map<String, Object> map, BufferPool bufferPool) {
         this.bufferPool = bufferPool;
         valueMap = new LinkedHashMap<>();
         populate(map);
+        rejectDuplicateKeys = false;
+    }
+    
+    JsonObjectBuilderImpl(Map<String, Object> map, BufferPool bufferPool, boolean rejectDuplicateKeys) {
+    	this.bufferPool = bufferPool;
+    	valueMap = new LinkedHashMap<>();
+    	populate(map);
+    	this.rejectDuplicateKeys = rejectDuplicateKeys;
     }
 
     @Override
@@ -184,7 +206,10 @@
         if (valueMap == null) {
             this.valueMap = new LinkedHashMap<>();
         }
-        valueMap.put(name, value);
+        JsonValue previousValue = valueMap.put(name, value);
+        if (rejectDuplicateKeys && previousValue != null) {
+            throw new IllegalStateException(JsonMessages.DUPLICATE_KEY(name));
+        }
     }
 
     private void validateName(String name) {
diff --git a/impl/src/main/java/org/glassfish/json/JsonParserImpl.java b/impl/src/main/java/org/glassfish/json/JsonParserImpl.java
index 23e86e7..62bf4d4 100644
--- a/impl/src/main/java/org/glassfish/json/JsonParserImpl.java
+++ b/impl/src/main/java/org/glassfish/json/JsonParserImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -23,6 +23,7 @@
 import java.math.BigDecimal;
 import java.nio.charset.Charset;
 import java.util.AbstractMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Spliterator;
@@ -55,25 +56,41 @@
 public class JsonParserImpl implements JsonParser {
 
     private final BufferPool bufferPool;
+    private final boolean rejectDuplicateKeys;
     private Context currentContext = new NoneContext();
     private Event currentEvent;
 
     private final Stack stack = new Stack();
     private final JsonTokenizer tokenizer;
-
+    
     public JsonParserImpl(Reader reader, BufferPool bufferPool) {
+        this(reader, bufferPool, false);
+    }
+
+    public JsonParserImpl(Reader reader, BufferPool bufferPool, boolean rejectDuplicateKeys) {
         this.bufferPool = bufferPool;
+        this.rejectDuplicateKeys = rejectDuplicateKeys;
         tokenizer = new JsonTokenizer(reader, bufferPool);
     }
 
     public JsonParserImpl(InputStream in, BufferPool bufferPool) {
+        this(in, bufferPool, false);
+    }
+
+    public JsonParserImpl(InputStream in, BufferPool bufferPool, boolean rejectDuplicateKeys) {
         this.bufferPool = bufferPool;
+        this.rejectDuplicateKeys = rejectDuplicateKeys;
         UnicodeDetectingInputStream uin = new UnicodeDetectingInputStream(in);
         tokenizer = new JsonTokenizer(new InputStreamReader(uin, uin.getCharset()), bufferPool);
     }
 
     public JsonParserImpl(InputStream in, Charset encoding, BufferPool bufferPool) {
+        this(in, encoding, bufferPool, false);
+    }
+
+    public JsonParserImpl(InputStream in, Charset encoding, BufferPool bufferPool, boolean rejectDuplicateKeys) {
         this.bufferPool = bufferPool;
+        this.rejectDuplicateKeys = rejectDuplicateKeys;
         tokenizer = new JsonTokenizer(new InputStreamReader(in, encoding), bufferPool);
     }
 
@@ -110,7 +127,7 @@
     }
 
     boolean isDefinitelyLong() {
-    	return tokenizer.isDefinitelyLong();
+        return tokenizer.isDefinitelyLong();
     }
 
     @Override
@@ -146,7 +163,7 @@
             throw new IllegalStateException(
                 JsonMessages.PARSER_GETOBJECT_ERR(currentEvent));
         }
-        return getObject(new JsonObjectBuilderImpl(bufferPool));
+        return getObject(new JsonObjectBuilderImpl(bufferPool, rejectDuplicateKeys));
     }
 
     @Override
@@ -175,7 +192,7 @@
             case END_ARRAY:
             case END_OBJECT:
             default:
-            	throw new IllegalStateException(JsonMessages.PARSER_GETVALUE_ERR(currentEvent));
+                throw new IllegalStateException(JsonMessages.PARSER_GETVALUE_ERR(currentEvent));
         }
     }
 
diff --git a/impl/src/main/java/org/glassfish/json/JsonProviderImpl.java b/impl/src/main/java/org/glassfish/json/JsonProviderImpl.java
index ce3627b..a4ebc8a 100644
--- a/impl/src/main/java/org/glassfish/json/JsonProviderImpl.java
+++ b/impl/src/main/java/org/glassfish/json/JsonProviderImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -17,6 +17,7 @@
 package org.glassfish.json;
 
 import org.glassfish.json.api.BufferPool;
+import org.glassfish.json.api.JsonConfig;
 
 import jakarta.json.*;
 import jakarta.json.stream.JsonGenerator;
@@ -41,7 +42,6 @@
  * @author Alex Soto
  */
 public class JsonProviderImpl extends JsonProvider {
-
     private final BufferPool bufferPool = new BufferPoolImpl();
 
     @Override
@@ -149,14 +149,27 @@
 
     @Override
     public JsonReaderFactory createReaderFactory(Map<String, ?> config) {
-        BufferPool pool = null;
-        if (config != null && config.containsKey(BufferPool.class.getName())) {
-            pool = (BufferPool)config.get(BufferPool.class.getName());
-        }
-        if (pool == null) {
+        Map<String, Object> providerConfig;
+        boolean rejectDuplicateKeys;
+        BufferPool pool;
+        if (config == null) {
+            providerConfig = Collections.emptyMap();
+            rejectDuplicateKeys = false;
             pool = bufferPool;
+        } else {
+            providerConfig = new HashMap<>();
+            if (rejectDuplicateKeys = JsonProviderImpl.isRejectDuplicateKeysEnabled(config)) {
+                providerConfig.put(JsonConfig.REJECT_DUPLICATE_KEYS, true);
+            }
+            pool = (BufferPool) config.get(BufferPool.class.getName());
+            if (pool != null) {
+                providerConfig.put(BufferPool.class.getName(), pool);
+            } else {
+                pool = bufferPool;
+            }
+            providerConfig = Collections.unmodifiableMap(providerConfig);
         }
-        return new JsonReaderFactoryImpl(pool);
+        return new JsonReaderFactoryImpl(providerConfig, pool, rejectDuplicateKeys);
     }
 
     @Override
@@ -255,18 +268,23 @@
     }
 
     @Override
-    public JsonBuilderFactory createBuilderFactory(Map<String,?> config) {
-        BufferPool pool = null ;
-        if (config != null && config.containsKey(BufferPool.class.getName())) {
-            pool = (BufferPool)config.get(BufferPool.class.getName());
-        }
-        if (pool == null) {
-            pool = bufferPool;
-        }
-        return new JsonBuilderFactoryImpl(pool);
+    public JsonBuilderFactory createBuilderFactory(Map<String, ?> config) {
+    	BufferPool pool = bufferPool;
+    	boolean rejectDuplicateKeys = false;
+    	if (config != null) {
+    		if (config.containsKey(BufferPool.class.getName())) {
+    			pool = (BufferPool) config.get(BufferPool.class.getName());
+    		}
+    		rejectDuplicateKeys = JsonProviderImpl.isRejectDuplicateKeysEnabled(config);
+    	}
+        return new JsonBuilderFactoryImpl(pool, rejectDuplicateKeys);
     }
 
     static boolean isPrettyPrintingEnabled(Map<String, ?> config) {
         return config.containsKey(JsonGenerator.PRETTY_PRINTING);
     }
+
+    static boolean isRejectDuplicateKeysEnabled(Map<String, ?> config) {
+        return config.containsKey(JsonConfig.REJECT_DUPLICATE_KEYS);
+    }
 }
diff --git a/impl/src/main/java/org/glassfish/json/JsonReaderFactoryImpl.java b/impl/src/main/java/org/glassfish/json/JsonReaderFactoryImpl.java
index 464fc8e..b039555 100644
--- a/impl/src/main/java/org/glassfish/json/JsonReaderFactoryImpl.java
+++ b/impl/src/main/java/org/glassfish/json/JsonReaderFactoryImpl.java
@@ -23,33 +23,35 @@
 import java.io.InputStream;
 import java.io.Reader;
 import java.nio.charset.Charset;
-import java.util.Collections;
 import java.util.Map;
 
 /**
  * @author Jitendra Kotamraju
  */
 class JsonReaderFactoryImpl implements JsonReaderFactory {
-    private final Map<String, ?> config = Collections.emptyMap();
+    private final Map<String, ?> config;
     private final BufferPool bufferPool;
+    private final boolean rejectDuplicateKeys;
 
-    JsonReaderFactoryImpl(BufferPool bufferPool) {
+    JsonReaderFactoryImpl(Map<String, ?> config, BufferPool bufferPool, boolean rejectDuplicateKeys) {
+        this.config = config;
         this.bufferPool = bufferPool;
+        this.rejectDuplicateKeys = rejectDuplicateKeys;
     }
 
     @Override
     public JsonReader createReader(Reader reader) {
-        return new JsonReaderImpl(reader, bufferPool);
+        return new JsonReaderImpl(reader, bufferPool, rejectDuplicateKeys);
     }
 
     @Override
     public JsonReader createReader(InputStream in) {
-        return new JsonReaderImpl(in, bufferPool);
+        return new JsonReaderImpl(in, bufferPool, rejectDuplicateKeys);
     }
 
     @Override
     public JsonReader createReader(InputStream in, Charset charset) {
-        return new JsonReaderImpl(in, charset, bufferPool);
+        return new JsonReaderImpl(in, charset, bufferPool, rejectDuplicateKeys);
     }
 
     @Override
diff --git a/impl/src/main/java/org/glassfish/json/JsonReaderImpl.java b/impl/src/main/java/org/glassfish/json/JsonReaderImpl.java
index 81b1e0a..ee73531 100644
--- a/impl/src/main/java/org/glassfish/json/JsonReaderImpl.java
+++ b/impl/src/main/java/org/glassfish/json/JsonReaderImpl.java
@@ -39,19 +39,31 @@
     private final JsonParserImpl parser;
     private boolean readDone;
     private final BufferPool bufferPool;
-
+    
     JsonReaderImpl(Reader reader, BufferPool bufferPool) {
-        parser = new JsonParserImpl(reader, bufferPool);
+        this(reader, bufferPool, false);
+    }
+
+    JsonReaderImpl(Reader reader, BufferPool bufferPool, boolean rejectDuplicateKeys) {
+        parser = new JsonParserImpl(reader, bufferPool, rejectDuplicateKeys);
         this.bufferPool = bufferPool;
     }
 
     JsonReaderImpl(InputStream in, BufferPool bufferPool) {
-        parser = new JsonParserImpl(in, bufferPool);
+        this(in, bufferPool, false);
+    }
+
+    JsonReaderImpl(InputStream in, BufferPool bufferPool, boolean rejectDuplicateKeys) {
+        parser = new JsonParserImpl(in, bufferPool, rejectDuplicateKeys);
         this.bufferPool = bufferPool;
     }
 
     JsonReaderImpl(InputStream in, Charset charset, BufferPool bufferPool) {
-        parser = new JsonParserImpl(in, charset, bufferPool);
+        this(in, charset, bufferPool, false);
+    }
+
+    JsonReaderImpl(InputStream in, Charset charset, BufferPool bufferPool, boolean rejectDuplicateKeys) {
+        parser = new JsonParserImpl(in, charset, bufferPool, rejectDuplicateKeys);
         this.bufferPool = bufferPool;
     }
 
diff --git a/impl/src/main/java/org/glassfish/json/api/JsonConfig.java b/impl/src/main/java/org/glassfish/json/api/JsonConfig.java
new file mode 100644
index 0000000..593c7d2
--- /dev/null
+++ b/impl/src/main/java/org/glassfish/json/api/JsonConfig.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.json.api;
+
+public interface JsonConfig {
+    /**
+     * Configuration property to reject duplicate keys. The value of the property could be
+     * be anything.
+     */
+    String REJECT_DUPLICATE_KEYS = "org.glassfish.json.rejectDuplicateKeys";
+}
diff --git a/impl/src/main/resources/org/glassfish/json/messages.properties b/impl/src/main/resources/org/glassfish/json/messages.properties
index bcc877f..3e4c349 100644
--- a/impl/src/main/resources/org/glassfish/json/messages.properties
+++ b/impl/src/main/resources/org/glassfish/json/messages.properties
@@ -42,6 +42,7 @@
 parser.scope.err=Cannot be called for value {0}
 parser.input.enc.detect.failed=Cannot auto-detect encoding, not enough chars
 parser.input.enc.detect.ioerr=I/O error while auto-detecting the encoding of stream
+parser.duplicate.key=Duplicate key ''{0}'' is not allowed
 
 generator.flush.io.err=I/O error while flushing generated JSON
 generator.close.io.err=I/O error while closing JsonGenerator
diff --git a/impl/src/test/java/org/glassfish/json/tests/JsonDuplicateKeyTest.java b/impl/src/test/java/org/glassfish/json/tests/JsonDuplicateKeyTest.java
new file mode 100644
index 0000000..c95d04a
--- /dev/null
+++ b/impl/src/test/java/org/glassfish/json/tests/JsonDuplicateKeyTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2020, 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.json.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.StringReader;
+import java.util.Collections;
+
+import org.glassfish.json.api.JsonConfig;
+import org.junit.Test;
+
+import jakarta.json.Json;
+import jakarta.json.JsonBuilderFactory;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonObjectBuilder;
+import jakarta.json.JsonReader;
+import jakarta.json.JsonReaderFactory;
+import jakarta.json.stream.JsonParsingException;
+
+public class JsonDuplicateKeyTest {
+    @Test
+    public void testJsonReaderDuplicateKey1() {
+        String json = "{\"a\":\"b\",\"a\":\"c\"}";
+        JsonReader jsonReader = Json.createReader(new StringReader(json));
+        JsonObject jsonObject = jsonReader.readObject();
+        assertEquals(jsonObject.getString("a"), "c");
+    }
+
+    @Test
+    public void testJsonReaderDuplicateKey2() {
+        String json = "{\"a\":\"b\",\"a\":\"c\"}";
+        JsonReaderFactory jsonReaderFactory = Json.createReaderFactory(Collections.singletonMap(JsonConfig.REJECT_DUPLICATE_KEYS, true));
+        JsonReader jsonReader = jsonReaderFactory.createReader(new StringReader(json));
+        try {
+            jsonReader.readObject();
+            fail();
+        } catch (Exception e) {
+            assertTrue(e instanceof JsonParsingException);
+            assertEquals("Duplicate key 'a' is not allowed", e.getMessage());
+        }
+    }
+    
+    @Test
+    public void testJsonObjectBuilderDuplcateKey1() {
+    	JsonObjectBuilder objectBuilder = Json.createObjectBuilder();
+    	JsonObject jsonObject = objectBuilder.add("a", "b").add("a", "c").build();
+    	assertEquals(jsonObject.getString("a"), "c");
+    }
+    
+    @Test
+    public void testJsonObjectBuilderDuplcateKey2() {
+    	JsonBuilderFactory jsonBuilderFactory = Json.createBuilderFactory(Collections.singletonMap(JsonConfig.REJECT_DUPLICATE_KEYS, true));
+    	JsonObjectBuilder objectBuilder = jsonBuilderFactory.createObjectBuilder();
+    	try {
+    		objectBuilder.add("a", "b").add("a", "c").build();
+    		fail();
+    	} catch (Exception e) {
+            assertTrue(e instanceof IllegalStateException);
+            assertEquals("Duplicate key 'a' is not allowed", e.getMessage());
+    	}
+    }
+}
diff --git a/pom.xml b/pom.xml
index 4dda0bd..0bf38ce 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-    Copyright (c) 2011, 2020 Oracle and/or its affiliates. All rights reserved.
+    Copyright (c) 2011, 2021 Oracle and/or its affiliates. All rights reserved.
 
     This program and the accompanying materials are made available under the
     terms of the Eclipse Public License v. 2.0, which is available at