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