From 41c61a8d56537e3de331c06967256f50d6b511cf Mon Sep 17 00:00:00 2001 From: "David M. Lloyd" Date: Sat, 15 Dec 2018 08:33:02 -0600 Subject: [PATCH] [#70] Implement a helper Map implementation for ConfigSources to use --- .../io/smallrye/config/ConfigSourceMap.java | 161 ++++++++++++ .../smallrye/config/ConfigSourceMapTest.java | 232 ++++++++++++++++++ 2 files changed, 393 insertions(+) create mode 100644 implementation/src/main/java/io/smallrye/config/ConfigSourceMap.java create mode 100644 implementation/src/test/java/io/smallrye/config/ConfigSourceMapTest.java diff --git a/implementation/src/main/java/io/smallrye/config/ConfigSourceMap.java b/implementation/src/main/java/io/smallrye/config/ConfigSourceMap.java new file mode 100644 index 000000000..cc03e10d4 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigSourceMap.java @@ -0,0 +1,161 @@ +/* + * Copyright 2018 Red Hat, Inc. + * + * Licensed 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 io.smallrye.config; + +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +/** + * A {@link Map Map<String, String>} which is backed by a {@link ConfigSource}. + * This should not be used to implement {@link ConfigSource#getProperties()} on {@code ConfigSource} + * instances which do not override {@code getPropertyNames()}, as this will result in infinite recursion. + * + * @implNote The key set of the map is the result of calling {@link ConfigSource#getPropertyNames()}; the rest + * of the map operations are derived from this method and {@link ConfigSource#getValue(String)}. + * The values collection and entry set are instantiated lazily and cached. + * The implementation attempts to make no assumptions about the efficiency of the backing implementation and + * prefers the most direct access possible. + *

+ * The backing collections are assumed to be immutable. + */ +public class ConfigSourceMap extends AbstractMap implements Map { + private final ConfigSource delegate; + private Values values; + private EntrySet entrySet; + + /** + * Construct a new instance. + * + * @param delegate the delegate configuration source (must not be {@code null}) + */ + public ConfigSourceMap(final ConfigSource delegate) { + this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); + } + + public int size() { + return delegate.getPropertyNames().size(); + } + + public boolean isEmpty() { + // may be cheaper in some cases + return delegate.getPropertyNames().isEmpty(); + } + + public boolean containsKey(final Object key) { + //noinspection SuspiciousMethodCalls - it's OK in this case + return delegate.getPropertyNames().contains(key); + } + + public String get(final Object key) { + return key instanceof String ? delegate.getValue((String) key) : null; + } + + public Set keySet() { + return delegate.getPropertyNames(); + } + + public Collection values() { + Values values = this.values; + if (values == null) return this.values = new Values(); + return values; + } + + public Set> entrySet() { + EntrySet entrySet = this.entrySet; + if (entrySet == null) return this.entrySet = new EntrySet(); + return entrySet; + } + + public void forEach(final BiConsumer action) { + // superclass is implemented in terms of entry set - expensive! + for (String name : keySet()) { + action.accept(name, delegate.getValue(name)); + } + } + + final class Values extends AbstractCollection implements Collection { + public Iterator iterator() { + return new Itr(delegate.getPropertyNames().iterator()); + } + + public int size() { + return delegate.getPropertyNames().size(); + } + + public boolean isEmpty() { + // may be cheaper in some cases + return delegate.getPropertyNames().isEmpty(); + } + + final class Itr implements Iterator { + private final Iterator iterator; + + Itr(final Iterator iterator) { + this.iterator = iterator; + } + + public boolean hasNext() { + return iterator.hasNext(); + } + + public String next() { + return delegate.getValue(iterator.next()); + } + } + } + + final class EntrySet extends AbstractSet> { + public Iterator> iterator() { + return new Itr(delegate.getPropertyNames().iterator()); + } + + public int size() { + return delegate.getPropertyNames().size(); + } + + public boolean isEmpty() { + // may be cheaper in some cases + return delegate.getPropertyNames().isEmpty(); + } + + final class Itr implements Iterator> { + private final Iterator iterator; + + Itr(final Iterator iterator) { + this.iterator = iterator; + } + + public boolean hasNext() { + return iterator.hasNext(); + } + + public Entry next() { + String name = iterator.next(); + return new SimpleImmutableEntry<>(name, delegate.getValue(name)); + } + } + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ConfigSourceMapTest.java b/implementation/src/test/java/io/smallrye/config/ConfigSourceMapTest.java new file mode 100644 index 000000000..91eed6d7b --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigSourceMapTest.java @@ -0,0 +1,232 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2018 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed 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 io.smallrye.config; + +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 static org.junit.Assert.fail; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.junit.Test; + +/** + */ +@SuppressWarnings("MismatchedQueryAndUpdateOfCollection") +public class ConfigSourceMapTest { + @SuppressWarnings("serial") + private static final Map MANY_MAP = new HashMap() {{ + put("key-one", "12345"); + put("null-valued-key", null); + put("test", "bar"); + put("fruit", "banana"); + }}; + private static final ConfigSource MANY_CONF_SRC = new PropertiesConfigSource(MANY_MAP, "test", 100); + private static final Map ONE_MAP = Collections.singletonMap("test", "foo"); + private static final ConfigSource ONE_CONF_SRC = new PropertiesConfigSource(ONE_MAP, "test", 100); + + @Test + public void testClear() { + try { + new ConfigSourceMap(ONE_CONF_SRC).clear(); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testCompute() { + try { + new ConfigSourceMap(ONE_CONF_SRC).compute("piano", (k, v) -> "player"); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testComputeIfAbsent() { + try { + //noinspection ExcessiveLambdaUsage + new ConfigSourceMap(ONE_CONF_SRC).computeIfAbsent("piano", k -> "player"); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testComputeIfPresent() { + try { + new ConfigSourceMap(ONE_CONF_SRC).computeIfPresent("test", (k, v) -> "bar"); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testContainsKey() { + final ConfigSourceMap csm = new ConfigSourceMap(MANY_CONF_SRC); + assertTrue(csm.containsKey("test")); + assertTrue(csm.containsKey("null-valued-key")); + assertFalse(csm.containsKey("missing-key")); + } + + @Test + public void testContainsValue() { + final ConfigSourceMap csm = new ConfigSourceMap(MANY_CONF_SRC); + assertTrue(csm.containsValue("12345")); + assertFalse(csm.containsValue("apple")); + assertTrue(csm.containsValue(null)); + } + + @Test + public void testEquals() { + assertEquals(MANY_MAP, new ConfigSourceMap(MANY_CONF_SRC)); + } + + @Test + public void testForEach() { + final Set need = new HashSet<>(MANY_MAP.keySet()); + final ConfigSourceMap csm = new ConfigSourceMap(MANY_CONF_SRC); + csm.forEach((k, v) -> { + assertEquals(MANY_MAP.get(k), v); + assertTrue(need.remove(k)); + }); + assertTrue("keys left in set", need.isEmpty()); + } + + @Test + public void testGet() { + final ConfigSourceMap csm = new ConfigSourceMap(MANY_CONF_SRC); + assertEquals("bar", csm.get("test")); + assertNull(csm.get("null-valued-key")); + assertNull(csm.get("nope")); + } + + @Test + public void testGetOrDefault() { + final ConfigSourceMap csm = new ConfigSourceMap(MANY_CONF_SRC); + assertEquals("bar", csm.getOrDefault("test", "foo")); + assertNull(csm.getOrDefault("null-valued-key", "oops")); + assertEquals("yes", csm.getOrDefault("nope", "yes")); + } + + @Test + public void testHashCode() { + assertEquals(MANY_MAP.hashCode(), new ConfigSourceMap(MANY_CONF_SRC).hashCode()); + } + + @Test + public void testIsEmpty() { + assertTrue(new ConfigSourceMap(new PropertiesConfigSource(Collections.emptyMap(), "test", 100)).isEmpty()); + assertFalse(new ConfigSourceMap(ONE_CONF_SRC).isEmpty()); + assertFalse(new ConfigSourceMap(MANY_CONF_SRC).isEmpty()); + } + + @Test + public void testKeySet() { + assertEquals(ONE_CONF_SRC.getPropertyNames(), new ConfigSourceMap(ONE_CONF_SRC).keySet()); + assertEquals(MANY_CONF_SRC.getPropertyNames(), new ConfigSourceMap(MANY_CONF_SRC).keySet()); + } + + @Test + public void testMerge() { + try { + new ConfigSourceMap(MANY_CONF_SRC).merge("test", "bar", (k, v) -> "oops"); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testPut() { + try { + new ConfigSourceMap(ONE_CONF_SRC).put("bees", "bzzzz"); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testPutAll() { + try { + new ConfigSourceMap(ONE_CONF_SRC).putAll(Collections.singletonMap("bees", "bzzzz")); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testPutIfAbsent() { + try { + new ConfigSourceMap(ONE_CONF_SRC).putIfAbsent("bees", "bzzzz"); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + new ConfigSourceMap(ONE_CONF_SRC).putIfAbsent("test", "not absent"); + } + + @Test + public void testRemove1() { + try { + new ConfigSourceMap(MANY_CONF_SRC).remove("test"); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testRemove2() { + try { + new ConfigSourceMap(MANY_CONF_SRC).remove("test"); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testReplace2() { + try { + new ConfigSourceMap(MANY_CONF_SRC).replace("test", "oops"); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testReplace3() { + try { + new ConfigSourceMap(MANY_CONF_SRC).replace("test", "bar", "oops"); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + try { + // false or exception are OK + assertFalse(new ConfigSourceMap(MANY_CONF_SRC).replace("test", "nope", "oops")); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testReplaceAll() { + try { + new ConfigSourceMap(MANY_CONF_SRC).replaceAll((k, v) -> "oops"); + fail("Expected exception"); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testSize() { + assertEquals(MANY_MAP.size(), new ConfigSourceMap(MANY_CONF_SRC).size()); + assertEquals(ONE_MAP.size(), new ConfigSourceMap(ONE_CONF_SRC).size()); + } +}