Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#70] Implement a helper Map implementation for ConfigSources to use #72

Merged
merged 1 commit into from
Dec 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions implementation/src/main/java/io/smallrye/config/ConfigSourceMap.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* The backing collections are assumed to be immutable.
*/
public class ConfigSourceMap extends AbstractMap<String, String> implements Map<String, String> {
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<String> keySet() {
return delegate.getPropertyNames();
}

public Collection<String> values() {
Values values = this.values;
if (values == null) return this.values = new Values();
return values;
}

public Set<Entry<String, String>> entrySet() {
EntrySet entrySet = this.entrySet;
if (entrySet == null) return this.entrySet = new EntrySet();
return entrySet;
}

public void forEach(final BiConsumer<? super String, ? super String> 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<String> implements Collection<String> {
public Iterator<String> 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<String> {
private final Iterator<String> iterator;

Itr(final Iterator<String> iterator) {
this.iterator = iterator;
}

public boolean hasNext() {
return iterator.hasNext();
}

public String next() {
return delegate.getValue(iterator.next());
}
}
}

final class EntrySet extends AbstractSet<Entry<String, String>> {
public Iterator<Entry<String, String>> 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<Entry<String, String>> {
private final Iterator<String> iterator;

Itr(final Iterator<String> iterator) {
this.iterator = iterator;
}

public boolean hasNext() {
return iterator.hasNext();
}

public Entry<String, String> next() {
String name = iterator.next();
return new SimpleImmutableEntry<>(name, delegate.getValue(name));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> MANY_MAP = new HashMap<String, String>() {{
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<String, String> 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<String> 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());
}
}