You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by cz...@apache.org on 2021/01/25 06:37:40 UTC
[felix-dev] branch master updated: FELIX-6380 : Make configuration
map case-insensitive
This is an automated email from the ASF dual-hosted git repository.
cziegeler pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/felix-dev.git
The following commit(s) were added to refs/heads/master by this push:
new 12c52d0 FELIX-6380 : Make configuration map case-insensitive
12c52d0 is described below
commit 12c52d0fd3d1e2a593c34ed347ec3937eb8ea3ce
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Mon Jan 25 07:37:26 2021 +0100
FELIX-6380 : Make configuration map case-insensitive
---
cm.json/pom.xml | 8 +
.../org/apache/felix/cm/json/Configurations.java | 2 +-
.../cm/json/impl/ConfigurationReaderImpl.java | 12 +-
.../felix/cm/json/impl/OrderedDictionary.java | 238 ++++++++++++++++++---
.../org/apache/felix/cm/json/package-info.java | 2 +-
.../cm/json/impl/ConfigurationReaderImplTest.java | 9 +
.../felix/cm/json/impl/OrderedDictionaryTest.java | 101 +++++++++
.../resources/configs/single-duplicatekeys.json | 4 +
8 files changed, 348 insertions(+), 28 deletions(-)
diff --git a/cm.json/pom.xml b/cm.json/pom.xml
index f706c55..c094735 100644
--- a/cm.json/pom.xml
+++ b/cm.json/pom.xml
@@ -60,6 +60,14 @@
</Bundle-SymbolicName>
</instructions>
</configuration>
+ <executions>
+ <execution>
+ <id>baseline</id>
+ <goals>
+ <goal>baseline</goal>
+ </goals>
+ </execution>
+ </executions>
</plugin>
<plugin>
<groupId>org.apache.rat</groupId>
diff --git a/cm.json/src/main/java/org/apache/felix/cm/json/Configurations.java b/cm.json/src/main/java/org/apache/felix/cm/json/Configurations.java
index 8bdd1bd..ba66b31 100644
--- a/cm.json/src/main/java/org/apache/felix/cm/json/Configurations.java
+++ b/cm.json/src/main/java/org/apache/felix/cm/json/Configurations.java
@@ -72,7 +72,7 @@ public class Configurations {
/**
* Create a new map for configuration properties The returned map keeps the
- * order of properties added.
+ * order of properties added and is using case-insensitive keys.
*
* @return A new map
*/
diff --git a/cm.json/src/main/java/org/apache/felix/cm/json/impl/ConfigurationReaderImpl.java b/cm.json/src/main/java/org/apache/felix/cm/json/impl/ConfigurationReaderImpl.java
index 286e93e..91a0848 100644
--- a/cm.json/src/main/java/org/apache/felix/cm/json/impl/ConfigurationReaderImpl.java
+++ b/cm.json/src/main/java/org/apache/felix/cm/json/impl/ConfigurationReaderImpl.java
@@ -269,7 +269,11 @@ public class ConfigurationReaderImpl
if ( keyInfo.isInternal ) {
final Object value = JsonSupport.convertToObject(propEntry.getValue());
if ( propertyHandler == null ) {
- properties.put(keyInfo.propertyKey, value);
+ if ( properties.put(keyInfo.propertyKey, value) != null ) {
+ throwIOException("PID ".concat(pid)
+ .concat(" : Duplicate property (properties are case-insensitive) : ")
+ .concat(keyInfo.propertyKey));
+ }
} else {
propertyHandler.handleConfiguratorProperty(pid, keyInfo.propertyKey, value);
}
@@ -310,7 +314,11 @@ public class ConfigurationReaderImpl
if ( convertedVal == null ) {
valid = false;
} else {
- properties.put(keyInfo.propertyKey, convertedVal);
+ if ( properties.put(keyInfo.propertyKey, convertedVal) != null ) {
+ throwIOException("PID ".concat(pid)
+ .concat(" : Duplicate property (properties are case-insensitive) : ")
+ .concat(keyInfo.propertyKey));
+ }
}
}
}
diff --git a/cm.json/src/main/java/org/apache/felix/cm/json/impl/OrderedDictionary.java b/cm.json/src/main/java/org/apache/felix/cm/json/impl/OrderedDictionary.java
index 5c3f8b4..9411108 100644
--- a/cm.json/src/main/java/org/apache/felix/cm/json/impl/OrderedDictionary.java
+++ b/cm.json/src/main/java/org/apache/felix/cm/json/impl/OrderedDictionary.java
@@ -19,6 +19,7 @@
package org.apache.felix.cm.json.impl;
import java.io.Serializable;
+import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
@@ -26,6 +27,7 @@ import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/**
@@ -38,10 +40,10 @@ public class OrderedDictionary extends Hashtable<String, Object> implements Seri
private static final long serialVersionUID = -525111601546803041L;
- private static class EnumarationImpl<E> implements Enumeration<E> {
- private final Iterator<E> iterator;
+ private static class EnumarationImpl implements Enumeration<String> {
+ private final Iterator<CaseInsensitiveKey> iterator;
- public EnumarationImpl(Iterator<E> iterator) {
+ public EnumarationImpl(final Iterator<CaseInsensitiveKey> iterator) {
this.iterator = iterator;
}
@@ -51,12 +53,12 @@ public class OrderedDictionary extends Hashtable<String, Object> implements Seri
}
@Override
- public E nextElement() {
- return iterator.next();
+ public String nextElement() {
+ return iterator.next().value;
}
}
- private Map<String, Object> map = Collections.synchronizedMap(new LinkedHashMap<String, Object>());
+ private Map<CaseInsensitiveKey, Object> map = Collections.synchronizedMap(new LinkedHashMap<>());
@Override
public int size() {
@@ -69,48 +71,58 @@ public class OrderedDictionary extends Hashtable<String, Object> implements Seri
}
@Override
- public boolean containsKey(Object key) {
- return map.containsKey(key);
+ public boolean containsKey(final Object key) {
+ if ( key == null ) {
+ return false;
+ }
+ return map.containsKey(new CaseInsensitiveKey(key.toString()));
}
@Override
- public boolean containsValue(Object value) {
+ public boolean containsValue(final Object value) {
return map.containsValue(value);
}
@Override
public Enumeration<String> keys() {
- return new EnumarationImpl<>(map.keySet().iterator());
+ return new EnumarationImpl(map.keySet().iterator());
}
@Override
public Enumeration<Object> elements() {
- return new EnumarationImpl<>(map.values().iterator());
+ return Collections.enumeration(map.values());
}
@Override
- public Object get(Object key) {
- return map.get(key);
+ public Object get(final Object key) {
+ if ( key == null ) {
+ return null;
+ }
+ return map.get(new CaseInsensitiveKey(key.toString()));
}
@Override
- public Object put(String key, Object value) {
+ public Object put(final String key, final Object value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
-
- return map.put(key, value);
+ final CaseInsensitiveKey k = new CaseInsensitiveKey(key);
+ final Object oldValue = this.map.remove(k);
+ this.map.put(k, value);
+ return oldValue;
}
@Override
- public Object remove(Object key) {
- return map.remove(key);
+ public Object remove(final Object key) {
+ return map.remove(new CaseInsensitiveKey(key.toString()));
}
@Override
- public void putAll(Map<? extends String, ? extends Object> m) {
- map.putAll(m);
+ public void putAll(final Map<? extends String, ? extends Object> m) {
+ for(final Map.Entry<? extends String, ? extends Object> e : m.entrySet()) {
+ this.put(e.getKey(), e.getValue());
+ }
}
@Override
@@ -120,21 +132,21 @@ public class OrderedDictionary extends Hashtable<String, Object> implements Seri
@Override
public Set<String> keySet() {
- return map.keySet();
+ return new KeySet();
}
@Override
public Collection<Object> values() {
- return map.values();
+ return this.map.values();
}
@Override
public Set<java.util.Map.Entry<String, Object>> entrySet() {
- return map.entrySet();
+ return new EntrySet();
}
@Override
- public boolean equals(Object o) {
+ public boolean equals(final Object o) {
return map.equals(o);
}
@@ -142,4 +154,182 @@ public class OrderedDictionary extends Hashtable<String, Object> implements Seri
public int hashCode() {
return map.hashCode();
}
+
+ private static final class CaseInsensitiveKey {
+
+ private final String value;
+
+ private final int hashCode;
+
+ public CaseInsensitiveKey(final String v) {
+ this.value = v;
+ this.hashCode = v.toUpperCase().hashCode();
+ }
+
+ @Override
+ public int hashCode() {
+ return this.hashCode;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof CaseInsensitiveKey)) {
+ return false;
+ }
+ final CaseInsensitiveKey other = (CaseInsensitiveKey) obj;
+ if ( value == null ) {
+ if ( other.value == null ) {
+ return true;
+ }
+ return false;
+ }
+ if ( other.value == null ) {
+ return false;
+ }
+ return value.equalsIgnoreCase(other.value);
+ }
+ }
+
+ private final class KeySet extends AbstractSet<String> {
+
+ @Override
+ public int size() {
+ return OrderedDictionary.this.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return OrderedDictionary.this.isEmpty();
+ }
+
+ @Override
+ public boolean contains(final Object o) {
+ return OrderedDictionary.this.containsKey(o);
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ return new KeyIterator(OrderedDictionary.this.map.keySet());
+ }
+
+ @Override
+ public boolean remove(final Object o) {
+ return OrderedDictionary.this.remove(o) != null;
+ }
+
+ @Override
+ public void clear() {
+ OrderedDictionary.this.clear();
+ }
+ }
+
+ private static final class KeyIterator implements Iterator<String> {
+ private final Iterator<CaseInsensitiveKey> i;
+
+ KeyIterator(final Collection<CaseInsensitiveKey> c) {
+ this.i = c.iterator();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return i.hasNext();
+ }
+
+ @Override
+ public String next() {
+ final CaseInsensitiveKey k = i.next();
+ return k.value;
+ }
+
+ @Override
+ public void remove() {
+ i.remove();
+ }
+ }
+
+ private final class EntrySet extends AbstractSet<Entry<String, Object>> {
+
+ @Override
+ public int size() {
+ return OrderedDictionary.this.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return OrderedDictionary.this.isEmpty();
+ }
+
+ @Override
+ public Iterator<Entry<String, Object>> iterator() {
+ return new EntryIterator(OrderedDictionary.this.map.entrySet());
+ }
+
+ @Override
+ public void clear() {
+ OrderedDictionary.this.clear();
+ }
+ }
+
+ private static final class EntryIterator implements Iterator<Entry<String, Object>> {
+ private final Iterator<Entry<CaseInsensitiveKey, Object>> i;
+
+ EntryIterator(final Collection<Entry<CaseInsensitiveKey, Object>> c) {
+ this.i = c.iterator();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return i.hasNext();
+ }
+
+ @Override
+ public Entry<String, Object> next() {
+ return new CaseInsentiveEntry(i.next());
+ }
+
+ @Override
+ public void remove() {
+ i.remove();
+ }
+ }
+
+ private static final class CaseInsentiveEntry implements Entry<String, Object> {
+ private final Entry<CaseInsensitiveKey, Object> entry;
+
+ CaseInsentiveEntry(final Entry<CaseInsensitiveKey, Object> entry) {
+ this.entry = entry;
+ }
+
+ @Override
+ public String getKey() {
+ return entry.getKey().value;
+ }
+
+ @Override
+ public Object getValue() {
+ return entry.getValue();
+ }
+
+ @Override
+ public Object setValue(final Object value) {
+ return entry.setValue(value);
+ }
+
+ @Override
+ public int hashCode() {
+ return entry.hashCode();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj instanceof CaseInsentiveEntry) {
+ final CaseInsentiveEntry other = (CaseInsentiveEntry) obj;
+ return Objects.equals(other.entry.getKey(), this.entry.getKey()) && Objects.equals(other.entry.getValue(), this.entry.getValue());
+ }
+ return false;
+ }
+ }
}
diff --git a/cm.json/src/main/java/org/apache/felix/cm/json/package-info.java b/cm.json/src/main/java/org/apache/felix/cm/json/package-info.java
index 4f9f76a..92e5b56 100644
--- a/cm.json/src/main/java/org/apache/felix/cm/json/package-info.java
+++ b/cm.json/src/main/java/org/apache/felix/cm/json/package-info.java
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-@Version("1.0.0")
+@Version("1.0.2")
package org.apache.felix.cm.json;
import org.osgi.annotation.versioning.Version;
\ No newline at end of file
diff --git a/cm.json/src/test/java/org/apache/felix/cm/json/impl/ConfigurationReaderImplTest.java b/cm.json/src/test/java/org/apache/felix/cm/json/impl/ConfigurationReaderImplTest.java
index 91162b3..37e8f8d 100644
--- a/cm.json/src/test/java/org/apache/felix/cm/json/impl/ConfigurationReaderImplTest.java
+++ b/cm.json/src/test/java/org/apache/felix/cm/json/impl/ConfigurationReaderImplTest.java
@@ -106,4 +106,13 @@ public class ConfigurationReaderImplTest {
assertTrue(ioe.getCause() instanceof JsonException);
}
}
+
+ @Test(expected = IOException.class)
+ public void testReadSingleConfigurationDuplicateKeys() throws IOException {
+ final ConfigurationReaderImpl cfgReader = new ConfigurationReaderImpl();
+ try (final InputStream jsonStream = this.getClass().getResourceAsStream("/configs/single-duplicatekeys.json");
+ final Reader jsonReader = new InputStreamReader(jsonStream, StandardCharsets.UTF_8)) {
+ cfgReader.build(jsonReader).readConfiguration();
+ }
+ }
}
diff --git a/cm.json/src/test/java/org/apache/felix/cm/json/impl/OrderedDictionaryTest.java b/cm.json/src/test/java/org/apache/felix/cm/json/impl/OrderedDictionaryTest.java
new file mode 100644
index 0000000..1b9445b
--- /dev/null
+++ b/cm.json/src/test/java/org/apache/felix/cm/json/impl/OrderedDictionaryTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.cm.json.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.junit.Test;
+
+public class OrderedDictionaryTest {
+
+ @Test public void testKeys() {
+ final Map<String, Object> map = new OrderedDictionary();
+ assertNull(map.put("hello", "helloV"));
+ assertNull(map.put("world", "worldV"));
+
+ assertEquals(2, map.size());
+ assertEquals(2, map.values().size());
+ assertTrue(map.values().contains("helloV"));
+ assertTrue(map.values().contains("worldV"));
+
+ assertEquals(2, map.entrySet().size());
+ final Map<String, Object> m = map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
+ assertEquals("helloV", m.get("hello"));
+ assertEquals("worldV", m.get("world"));
+
+ Set<String> keys = new HashSet<>(map.keySet());
+ assertEquals(2, keys.size());
+ assertTrue(keys.contains("hello"));
+ assertTrue(keys.contains("world"));
+
+ assertEquals("helloV", map.get("hello"));
+ assertEquals("worldV", map.get("world"));
+ assertNull(map.get("foo"));
+
+ assertEquals("helloV", map.get("HELLO"));
+ assertEquals("worldV", map.get("WORLD"));
+
+ assertEquals("helloV", map.put("heLLo", "bar"));
+ assertEquals(2, map.size());
+
+ keys = new HashSet<>(map.keySet());
+ assertEquals(2, keys.size());
+ assertTrue(keys.contains("heLLo"));
+ assertTrue(keys.contains("world"));
+
+ assertEquals("bar", map.get("hello"));
+ assertEquals("worldV", map.get("world"));
+
+ assertEquals("bar", map.remove("HellO"));
+ assertEquals("worldV", map.remove("WoRlD"));
+ assertTrue(map.isEmpty());
+ }
+
+ @Test public void testClear() {
+ final Map<String, Object> map = new OrderedDictionary();
+ map.put("hello", "hello");
+ map.put("world", "world");
+
+ map.clear();
+ assertTrue(map.isEmpty());
+ assertEquals(0, map.size());
+ assertTrue(map.values().isEmpty());
+ assertTrue(map.keySet().isEmpty());
+ assertTrue(map.entrySet().isEmpty());
+ }
+
+ @Test public void testContains() {
+ final Map<String, Object> map = new OrderedDictionary();
+ map.put("hello", "helloV");
+
+ assertTrue(map.containsKey("hello"));
+ assertTrue(map.containsKey("heLLo"));
+ assertFalse(map.containsKey("hell o"));
+
+ assertTrue(map.containsValue("helloV"));
+ assertFalse(map.containsValue("heLLoV"));
+ assertFalse(map.containsValue("hello"));
+ }
+}
\ No newline at end of file
diff --git a/cm.json/src/test/resources/configs/single-duplicatekeys.json b/cm.json/src/test/resources/configs/single-duplicatekeys.json
new file mode 100644
index 0000000..44f4625
--- /dev/null
+++ b/cm.json/src/test/resources/configs/single-duplicatekeys.json
@@ -0,0 +1,4 @@
+{
+ "text" : "Hello World",
+ "teXT" : "hello WORLD"
+}
\ No newline at end of file