You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tamaya.apache.org by an...@apache.org on 2018/11/18 21:20:33 UTC
[15/20] incubator-tamaya-extensions git commit: Moved collections,
consul,
etcd and hazelcast modules into extensions. Updated docs to match code.
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/collections/src/test/java/org/apache/tamaya/collections/CollectionsTypedTests.java
----------------------------------------------------------------------
diff --git a/modules/collections/src/test/java/org/apache/tamaya/collections/CollectionsTypedTests.java b/modules/collections/src/test/java/org/apache/tamaya/collections/CollectionsTypedTests.java
new file mode 100644
index 0000000..92c592e
--- /dev/null
+++ b/modules/collections/src/test/java/org/apache/tamaya/collections/CollectionsTypedTests.java
@@ -0,0 +1,207 @@
+/*
+ * 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.tamaya.collections;
+
+import org.apache.tamaya.Configuration;
+import org.apache.tamaya.TypeLiteral;
+import org.junit.Test;
+
+import java.util.*;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Basic tests for Tamaya collection support. Relevant configs for this tests:
+ * <pre>base.items=1,2,3,4,5,6,7,8,9,0
+ * base.map=1::a, 2::b, 3::c, [4:: ]
+ * </pre>
+ */
+public class CollectionsTypedTests {
+
+ @Test
+ public void testArrayListList_String(){
+ Configuration config = Configuration.current();
+ List<String> items = config.get("typed2.arraylist", new TypeLiteral<List<String>>(){});
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof ArrayList);
+ items = (List<String>) config.get("typed2.arraylist", List.class);
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof ArrayList);
+ }
+
+ @Test
+ public void testLinkedListList_String(){
+ Configuration config = Configuration.current();
+ List<String> items = config.get("typed2.linkedlist", new TypeLiteral<List<String>>(){});
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof LinkedList);
+ items = (List<String>) config.get("typed2.linkedlist", List.class);
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof LinkedList);
+ }
+
+
+ @Test
+ public void testHashSet_String(){
+ Configuration config = Configuration.current();
+ Set<String> items = config.get("typed2.hashset", new TypeLiteral<Set<String>>(){});
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof HashSet);
+ items = (Set<String>) config.get("typed2.hashset", Set.class);
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof HashSet);
+ }
+
+ @Test
+ public void testTreeSet_String(){
+ Configuration config = Configuration.current();
+ Set<String> items = config.get("typed2.treeset", new TypeLiteral<Set<String>>(){});
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof TreeSet);
+ items = (Set<String>) config.get("typed2.treeset", Set.class);
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof TreeSet);
+ }
+
+ @Test
+ public void testHashMap_String(){
+ Configuration config = Configuration.current();
+ Map<String,String> items = config.get("typed2.hashmap", new TypeLiteral<Map<String,String>>(){});
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(4, items.size());
+ assertEquals("a", items.get("1"));
+ assertEquals("b", items.get("2"));
+ assertEquals("c", items.get("3"));
+ assertEquals(" ", items.get("4"));
+ assertTrue(items instanceof HashMap);
+ items = (Map<String,String>) config.get("typed2.hashmap", Map.class);
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(4, items.size());
+ assertEquals("a", items.get("1"));
+ assertEquals("b", items.get("2"));
+ assertEquals("c", items.get("3"));
+ assertEquals(" ", items.get("4"));
+ assertTrue(items instanceof HashMap);
+ }
+
+ @Test
+ public void testTreeMap_String(){
+ Configuration config = Configuration.current();
+ Map<String,String> items = config.get("typed2.treemap", new TypeLiteral<Map<String,String>>(){});
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(4, items.size());
+ assertEquals("a", items.get("1"));
+ assertEquals("b", items.get("2"));
+ assertEquals("c", items.get("3"));
+ assertEquals(" ", items.get("4"));
+ assertTrue(items instanceof TreeMap);
+ items = (Map<String,String>) config.get("typed2.treemap", Map.class);
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(4, items.size());
+ assertEquals("a", items.get("1"));
+ assertEquals("b", items.get("2"));
+ assertEquals("c", items.get("3"));
+ assertEquals(" ", items.get("4"));
+ assertTrue(items instanceof TreeMap);
+ }
+
+ @Test
+ public void testCollection_HashSet(){
+ Configuration config = Configuration.current();
+ Collection<String> items = config.get("typed2.hashset", new TypeLiteral<Collection<String>>(){});
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof HashSet);
+ items = (Collection<String>) config.get("typed2.hashset", Collection.class);
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof HashSet);
+ }
+
+ @Test
+ public void testCollection_TreeSet(){
+ Configuration config = Configuration.current();
+ Collection<String> items = config.get("typed2.treeset", new TypeLiteral<Collection<String>>(){});
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof TreeSet);
+ items = (Collection<String>) config.get("typed2.treeset", Collection.class);
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof TreeSet);
+ }
+
+ @Test
+ public void testCollection_ArrayList(){
+ Configuration config = Configuration.current();
+ Collection<String> items = config.get("typed2.arraylist", new TypeLiteral<Collection<String>>(){});
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof ArrayList);
+ items = (Collection<String>) config.get("typed2.arraylist", Collection.class);
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof ArrayList);
+ }
+
+ @Test
+ public void testCollection_LinkedList(){
+ Configuration config = Configuration.current();
+ Collection<String> items = config.get("typed2.linkedlist", new TypeLiteral<Collection<String>>(){});
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof LinkedList);
+ items = (Collection<String>) config.get("typed2.linkedlist", Collection.class);
+ assertNotNull(items);
+ assertFalse(items.isEmpty());
+ assertEquals(10, items.size());
+ assertTrue(items instanceof LinkedList);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/collections/src/test/java/org/apache/tamaya/collections/MyUpperCaseConverter.java
----------------------------------------------------------------------
diff --git a/modules/collections/src/test/java/org/apache/tamaya/collections/MyUpperCaseConverter.java b/modules/collections/src/test/java/org/apache/tamaya/collections/MyUpperCaseConverter.java
new file mode 100644
index 0000000..1c95261
--- /dev/null
+++ b/modules/collections/src/test/java/org/apache/tamaya/collections/MyUpperCaseConverter.java
@@ -0,0 +1,33 @@
+/*
+ * 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.tamaya.collections;
+
+import org.apache.tamaya.spi.ConversionContext;
+import org.apache.tamaya.spi.PropertyConverter;
+
+/**
+ * Example converter that is used for testing the custom parsing functionality. It sorrounds values with () and
+ * converts them to uppercase.
+ */
+public class MyUpperCaseConverter implements PropertyConverter<String>{
+ @Override
+ public String convert(String value, ConversionContext context) {
+ return "("+value.toUpperCase()+")";
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/collections/src/test/resources/META-INF/javaconfiguration.properties
----------------------------------------------------------------------
diff --git a/modules/collections/src/test/resources/META-INF/javaconfiguration.properties b/modules/collections/src/test/resources/META-INF/javaconfiguration.properties
new file mode 100644
index 0000000..3764840
--- /dev/null
+++ b/modules/collections/src/test/resources/META-INF/javaconfiguration.properties
@@ -0,0 +1,69 @@
+#
+# 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 current 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.
+#
+# Similar to etcd all keys starting with a _ are hidden by default (only directly accessible).
+
+# Config for base tests (no combination policy)
+base.items=1,2,3,4,5,6,7,8,9,0
+base.map=1=a, 2=b, 3=c, [4= ]
+
+# Config for tests with explcit implementation types
+typed2.arraylist=1,2,3,4,5,6,7,8,9,0
+[META]typed2.arraylist.collection-type=ArrayList
+typed2.linkedlist=1,2,3,4,5,6,7,8,9,0
+[META]typed2.linkedlist.collection-type=java.util.LinkedList
+typed2.hashset=1,2,3,4,5,6,7,8,9,0
+[META]typed2.hashset.collection-type=HashSet
+typed2.treeset=1,2,3,4,5,6,7,8,9,0
+[META]typed2.treeset.collection-type=TreeSet
+typed2.hashmap=1=a, 2=b, 3=c, [4= ]
+[META]typed2.hashmap.collection-type=java.util.HashMap
+typed2.treemap=1=a, 2=b, 3=c, [4= ]
+[META]typed2.treemap.collection-type=TreeMap
+
+# Config for tests with combination policy, writable
+typed.arraylist=1,2,3,4,5,6,7,8,9,0
+[META]typed.arraylist.collection-type=ArrayList
+[META]typed.arraylist.read-only=true
+typed.linkedlist=1,2,3,4,5,6,7,8,9,0
+[META]typed.linkedlist.collection-type=java.util.LinkedList
+typed.hashset=1,2,3,4,5,6,7,8,9,0
+[META]typed.hashset.collection-type=HashSet
+typed.treeset=1,2,3,4,5,6,7,8,9,0
+[META]typed.treeset.collection-type=TreeSet
+typed.hashmap=1=a, 2=b, 3=c, [4= ]
+[META]typed.hashmap.collection-type=java.util.HashMap
+[META]typed.hashmap.read-only=true
+typed.treemap=1=a, 2=b, 3=c, [4= ]
+[META]typed.treemap.collection-type=TreeMap
+
+# Config for advanced tests
+sep-list=a,b,c|d,e,f|g,h,i
+[META]sep-list.collection-type=List
+[META]sep-list.item-separator=|
+currency-list=CHF,USD,USS
+[META]currency-list.collection-type=List
+
+parser-list=a,b,c
+[META]parser-list.collection-type=List
+[META]parser-list.item-converter=org.apache.tamaya.collections.MyUpperCaseConverter
+
+redefined-map=0==none | 1==single | 2==any
+[META]redefined-map.map-entry-separator===
+[META]redefined-map.item-separator=|
+
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/consul/bnd.bnd
----------------------------------------------------------------------
diff --git a/modules/consul/bnd.bnd b/modules/consul/bnd.bnd
new file mode 100644
index 0000000..9928592
--- /dev/null
+++ b/modules/consul/bnd.bnd
@@ -0,0 +1,32 @@
+-buildpath: \
+ osgi.annotation; version=6.0.0,\
+ osgi.core; version=6.0,\
+ osgi.cmpn; version=6.0
+
+-testpath: \
+ ${junit}
+
+javac.source: 1.8
+javac.target: 1.8
+
+Automatic-Module-Name: org.apache.tamaya.consul
+Bundle-Version: ${version}.${tstamp}
+Bundle-Name: Apache Tamaya - Consul
+Bundle-SymbolicName: org.apache.tamaya.consul
+Bundle-Description: Apacha Tamaya Config - Consul PropertySource
+Bundle-Category: Implementation
+Bundle-Copyright: (C) Apache Foundation
+Bundle-License: Apache Licence version 2
+Bundle-Vendor: Apache Software Foundation
+Bundle-ContactAddress: dev-tamaya@incubator.apache.org
+Bundle-DocURL: http://tamaya.apache.org
+Export-Package: \
+ org.apache.tamaya.consul
+Import-Package: \
+ org.apache.tamaya,\
+ org.apache.tamaya.spi,\
+ org.apache.tamaya.mutableconfig,\
+ org.apache.tamaya.mutableconfig.spi
+Export-Service: \
+ org.apache.tamaya.spi.PropertySource
+
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/consul/pom.xml
----------------------------------------------------------------------
diff --git a/modules/consul/pom.xml b/modules/consul/pom.xml
new file mode 100644
index 0000000..aa8dfe4
--- /dev/null
+++ b/modules/consul/pom.xml
@@ -0,0 +1,83 @@
+<!--
+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 current 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.tamaya.ext</groupId>
+ <artifactId>tamaya-sandbox</artifactId>
+ <version>0.4-incubating-SNAPSHOT</version>
+ <relativePath>..</relativePath>
+ </parent>
+
+ <artifactId>tamaya-consul</artifactId>
+ <name>Apache Tamaya Modules - Consul PropertySource</name>
+ <packaging>jar</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>java-hamcrest</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.tamaya</groupId>
+ <artifactId>tamaya-core</artifactId>
+ <version>${project.parent.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tamaya</groupId>
+ <artifactId>tamaya-api</artifactId>
+ <version>${project.parent.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tamaya.ext</groupId>
+ <artifactId>tamaya-functions</artifactId>
+ <version>${project.parent.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tamaya.ext</groupId>
+ <artifactId>tamaya-mutable-config</artifactId>
+ <version>${project.parent.version}</version>
+ <scope>provided</scope>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>com.orbitz.consul</groupId>
+ <artifactId>consul-client</artifactId>
+ <version>0.17.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.cxf</groupId>
+ <artifactId>cxf-rt-rs-client</artifactId>
+ <version>3.2.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.cxf</groupId>
+ <artifactId>cxf-rt-transports-http-hc</artifactId>
+ <version>3.2.1</version>
+ </dependency>
+ </dependencies>
+
+</project>
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/consul/src/main/java/org/apache/tamaya/consul/AbstractConsulPropertySource.java
----------------------------------------------------------------------
diff --git a/modules/consul/src/main/java/org/apache/tamaya/consul/AbstractConsulPropertySource.java b/modules/consul/src/main/java/org/apache/tamaya/consul/AbstractConsulPropertySource.java
new file mode 100644
index 0000000..f07fb68
--- /dev/null
+++ b/modules/consul/src/main/java/org/apache/tamaya/consul/AbstractConsulPropertySource.java
@@ -0,0 +1,246 @@
+/*
+ * 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.tamaya.consul;
+
+import com.google.common.base.Optional;
+import com.google.common.net.HostAndPort;
+import com.orbitz.consul.Consul;
+import com.orbitz.consul.KeyValueClient;
+import com.orbitz.consul.model.kv.Value;
+import org.apache.tamaya.mutableconfig.ConfigChangeRequest;
+import org.apache.tamaya.mutableconfig.spi.MutablePropertySource;
+import org.apache.tamaya.spi.PropertyValue;
+import org.apache.tamaya.spisupport.propertysource.BasePropertySource;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * Propertysource base class that is reading configuration from a configured consul endpoint.
+ */
+public abstract class AbstractConsulPropertySource extends BasePropertySource
+implements MutablePropertySource{
+ private static final Logger LOG = Logger.getLogger(AbstractConsulPropertySource.class.getName());
+
+ private String prefix = "";
+
+ private List<HostAndPort> consulBackends = new ArrayList<>();
+
+ /** The config cache used. */
+ private Map<String, PropertyValue> configMap = new ConcurrentHashMap<>();
+
+ private AtomicLong timeoutDuration = new AtomicLong(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES));
+
+ private AtomicLong timeout = new AtomicLong();
+
+
+ public AbstractConsulPropertySource(){
+ this("consul");
+ }
+
+ public AbstractConsulPropertySource(String name){
+ super(name);
+ }
+
+ /**
+ * Get the current timeout, when a reload will be triggered on access.
+ * @return the current timeout, or 0 if no data has been loaded at all.
+ */
+ public long getValidUntil(){
+ return timeout.get();
+ }
+
+ /**
+ * Get the current cache timeout.
+ * @return the timeout duration after which data will be reloaded.
+ */
+ public long getCachePeriod(){
+ return timeoutDuration.get();
+ }
+
+ /**
+ * Set the duration after which the data cache will be reloaded.
+ * @param millis the millis
+ */
+ public void setCacheTimeout(long millis){
+ this.timeoutDuration.set(millis);
+ }
+
+ /**
+ * Gets the prefix that is added for looking up keys in consul. This allows to use a separate subnamespace in
+ * consul for configuration.
+ * @return the prefix, never null.
+ */
+ public String getPrefix() {
+ return prefix;
+ }
+
+ /**
+ * Sets the prefix that is added for looking up keys in consul. This allows to use a separate subnamespace in
+ * consul for configuration.
+ * @param prefix the prefix, not null.
+ */
+ public void setPrefix(String prefix) {
+ this.prefix = Objects.requireNonNull(prefix);
+ }
+
+ /**
+ * Set the consol server to connect to.
+ * @param server the server list, not null.
+ */
+ public void setServer(List<String> server){
+ if(!Objects.equals(getServer(), server)) {
+ List<HostAndPort> consulBackends = new ArrayList<>();
+ for (String s : server) {
+ consulBackends.add(HostAndPort.fromString(s));
+ }
+ this.consulBackends = consulBackends;
+ refresh();
+ }
+
+ }
+
+ /**
+ * Get a list of current servers.
+ * @return the server list, not null.
+ */
+ public List<String> getServer() {
+ return this.consulBackends.stream().map(s -> s.toString()).collect(Collectors.toList());
+ }
+
+ /**
+ * Checks for a cache timeout and optionally reloads the data.
+ */
+ public void checkRefresh(){
+ if(this.timeout.get() < System.currentTimeMillis()){
+ refresh();
+ }
+ }
+
+ /**
+ * Clears the cached entries.
+ */
+ public void refresh(){
+ this.configMap.clear();
+ }
+
+ @Override
+ public PropertyValue get(String key) {
+ checkRefresh();
+ String reqKey = key;
+ if(key.startsWith("[META]")){
+ reqKey = key.substring("[META]".length());
+ if(reqKey.endsWith(".createdIndex")){
+ reqKey = reqKey.substring(0,reqKey.length()-".createdIndex".length());
+ } else if(reqKey.endsWith(".modifiedIndex")){
+ reqKey = reqKey.substring(0,reqKey.length()-".modifiedIndex".length());
+ } else if(reqKey.endsWith(".ttl")){
+ reqKey = reqKey.substring(0,reqKey.length()-".ttl".length());
+ } else if(reqKey.endsWith(".expiration")){
+ reqKey = reqKey.substring(0,reqKey.length()-".expiration".length());
+ } else if(reqKey.endsWith(".source")){
+ reqKey = reqKey.substring(0,reqKey.length()-".source".length());
+ }
+ }
+ PropertyValue val = this.configMap.get(reqKey);
+ if(val!=null){
+ return val;
+ }
+ // check prefix, if key does not start with it, it is not part of our name space
+ // if so, the prefix part must be removedProperties, so etcd can resolve without it
+ for(HostAndPort hostAndPort: this.consulBackends){
+ try{
+ Consul consul = Consul.builder().withHostAndPort(hostAndPort).build();
+ KeyValueClient kvClient = consul.keyValueClient();
+ Optional<Value> valueOpt = kvClient.getValue(prefix + reqKey);
+ if(!valueOpt.isPresent()) {
+ LOG.log(Level.FINE, "key not found in consul: " + prefix + reqKey);
+ }else{
+ // No prefix mapping necessary here, since we only access/return the createValue...
+ Value value = valueOpt.get();
+ Map<String,String> props = new HashMap<>();
+ props.put("createIndex", String.valueOf(value.getCreateIndex()));
+ props.put("modifyIndex", String.valueOf(value.getModifyIndex()));
+ props.put("lockIndex", String.valueOf(value.getLockIndex()));
+ props.put("flags", String.valueOf(value.getFlags()));
+ props.put("source", getName());
+ val = PropertyValue.createValue(reqKey, value.getValue().get())
+ .setMeta(props);
+ break;
+ }
+ } catch(Exception e){
+ LOG.log(Level.FINE, "etcd access failed on " + hostAndPort + ", trying next...", e);
+ }
+ }
+ if(val!=null){
+ this.configMap.put(reqKey, val);
+ }
+ return val;
+ }
+
+ @Override
+ public Map<String, PropertyValue> getProperties() {
+ checkRefresh();
+ return Collections.unmodifiableMap(configMap);
+ }
+
+ @Override
+ public void applyChange(ConfigChangeRequest configChange) {
+ for(HostAndPort hostAndPort: this.consulBackends){
+ try{
+ Consul consul = Consul.builder().withHostAndPort(hostAndPort).build();
+ KeyValueClient kvClient = consul.keyValueClient();
+
+ for(String k: configChange.getRemovedProperties()){
+ try{
+ kvClient.deleteKey(k);
+ } catch(Exception e){
+ LOG.info("Failed to remove key from consul: " + k);
+ }
+ }
+ for(Map.Entry<String,String> en:configChange.getAddedProperties().entrySet()){
+ String key = en.getKey();
+ try{
+ kvClient.putValue(prefix + key,en.getValue());
+ }catch(Exception e) {
+ LOG.info("Failed to add key to consul: " + prefix + en.getKey() + "=" + en.getValue());
+ }
+ }
+ // success: stop here
+ break;
+ } catch(Exception e){
+ LOG.log(Level.FINE, "consul access failed on " + hostAndPort + ", trying next...", e);
+ }
+ }
+ }
+
+ @Override
+ protected String toStringValues() {
+ return super.toStringValues() +
+ " prefix=" + prefix + '\n' +
+ " cacheTimeout=" + timeout + '\n' +
+ " backends=" + this.consulBackends + '\n';
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/consul/src/main/java/org/apache/tamaya/consul/ConsulBackendConfig.java
----------------------------------------------------------------------
diff --git a/modules/consul/src/main/java/org/apache/tamaya/consul/ConsulBackendConfig.java b/modules/consul/src/main/java/org/apache/tamaya/consul/ConsulBackendConfig.java
new file mode 100644
index 0000000..c7b0c14
--- /dev/null
+++ b/modules/consul/src/main/java/org/apache/tamaya/consul/ConsulBackendConfig.java
@@ -0,0 +1,85 @@
+/*
+ * 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.tamaya.consul;
+
+import com.google.common.net.HostAndPort;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Singleton that reads and stores the current consul setup, especially the possible host:ports to be used.
+ */
+final class ConsulBackendConfig {
+
+ private static final Logger LOG = Logger.getLogger(ConsulBackendConfig.class.getName());
+ private static final String TAMAYA_CONSUL_SERVER_URLS = "tamaya.consul.server.urls";
+ private static final String TAMAYA_CONSUL_DIRECTORY = "tamaya.consul.directory";
+ private static final String TAMAYA_CONSUL_PREFIX = "tamaya.consul.prefix";
+
+
+ private ConsulBackendConfig(){}
+
+ public static String getConsulDirectory(){
+ String val = System.getProperty(TAMAYA_CONSUL_DIRECTORY);
+ if(val == null){
+ val = System.getenv(TAMAYA_CONSUL_DIRECTORY);
+ }
+ if(val!=null){
+ return val;
+ }
+ return "";
+ }
+
+ public static List<String> getServers(){
+ String serverURLs = System.getProperty(TAMAYA_CONSUL_SERVER_URLS);
+ if(serverURLs==null){
+ serverURLs = System.getenv(TAMAYA_CONSUL_SERVER_URLS);
+ }
+ if(serverURLs==null){
+ serverURLs = "http://127.0.0.1:4001";
+ }
+ List<String> servers = new ArrayList<>();
+ for(String url:serverURLs.split("\\,")) {
+ try{
+ servers.add(url.trim());
+ LOG.info("Using etcd endoint: " + url);
+ } catch(Exception e){
+ LOG.log(Level.SEVERE, "Error initializing etcd accessor for URL: " + url, e);
+ }
+ }
+ return servers;
+ }
+
+ public static String getPrefix() {
+ String val = System.getProperty(TAMAYA_CONSUL_PREFIX);
+ if(val == null){
+ val = System.getenv(TAMAYA_CONSUL_PREFIX);
+ }
+ if(val!=null){
+ return val;
+ }
+ return "";
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/consul/src/main/java/org/apache/tamaya/consul/ConsulPropertySource.java
----------------------------------------------------------------------
diff --git a/modules/consul/src/main/java/org/apache/tamaya/consul/ConsulPropertySource.java b/modules/consul/src/main/java/org/apache/tamaya/consul/ConsulPropertySource.java
new file mode 100644
index 0000000..ce92a95
--- /dev/null
+++ b/modules/consul/src/main/java/org/apache/tamaya/consul/ConsulPropertySource.java
@@ -0,0 +1,66 @@
+/*
+ * 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.tamaya.consul;
+
+import com.google.common.base.Optional;
+import com.google.common.net.HostAndPort;
+import com.orbitz.consul.Consul;
+import com.orbitz.consul.KeyValueClient;
+import com.orbitz.consul.model.kv.Value;
+import org.apache.tamaya.mutableconfig.ConfigChangeRequest;
+import org.apache.tamaya.mutableconfig.spi.MutablePropertySource;
+import org.apache.tamaya.spi.PropertyValue;
+import org.apache.tamaya.spisupport.propertysource.BasePropertySource;
+
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Propertysource that is reading configuration from a configured consul endpoint. Setting
+ * {@code consul.prefix} as system property maps the consul based configuration
+ * to this prefix namespace. Consul servers are configured as {@code consul.urls} system or environment property.
+ */
+public class ConsulPropertySource extends AbstractConsulPropertySource{
+ private static final Logger LOG = Logger.getLogger(ConsulPropertySource.class.getName());
+
+
+ public ConsulPropertySource(String prefix, List<String> backends){
+ this();
+ setPrefix(prefix);
+ setServer(backends);
+ }
+
+ public ConsulPropertySource(List<String> backends){
+ this();
+ setServer(backends);
+ }
+
+ public ConsulPropertySource(){
+ super();
+ setDefaultOrdinal(1000);
+ setPrefix(System.getProperty("tamaya.consul.prefix", ""));
+ }
+
+ public ConsulPropertySource(String... backends){
+ this();
+ setServer(Arrays.asList(backends));
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/consul/src/test/java/org/apache/tamaya/consul/ConsulPropertySourceTest.java
----------------------------------------------------------------------
diff --git a/modules/consul/src/test/java/org/apache/tamaya/consul/ConsulPropertySourceTest.java b/modules/consul/src/test/java/org/apache/tamaya/consul/ConsulPropertySourceTest.java
new file mode 100644
index 0000000..7b04dc0
--- /dev/null
+++ b/modules/consul/src/test/java/org/apache/tamaya/consul/ConsulPropertySourceTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.tamaya.consul;
+
+import org.apache.tamaya.consul.ConsulPropertySource;
+import org.apache.tamaya.spi.PropertyValue;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Created by atsticks on 07.01.16.
+ */
+public class ConsulPropertySourceTest {
+
+ private final ConsulPropertySource propertySource = new ConsulPropertySource();
+
+ @BeforeClass
+ public static void setup(){
+ System.setProperty("consul.urls", "http://127.0.0.1:8300");
+ }
+
+ @Test
+ public void testGetOrdinal() throws Exception {
+ assertEquals(1000, propertySource.getOrdinal());
+ }
+
+ @Test
+ public void testGetDefaultOrdinal() throws Exception {
+ assertEquals(1000, propertySource.getDefaultOrdinal());
+ }
+
+ @Test
+ public void testGetName() throws Exception {
+ assertEquals("consul", propertySource.getName());
+ }
+
+ @Test
+ public void testGet() throws Exception {
+ Map<String,PropertyValue> props = propertySource.getProperties();
+ for(Map.Entry<String,PropertyValue> en:props.entrySet()){
+ assertNotNull("Key not found: " + en.getKey(), propertySource.get(en.getKey()));
+ }
+ }
+
+ @Test
+ public void testGetProperties() throws Exception {
+ Map<String,PropertyValue> props = propertySource.getProperties();
+ assertNotNull(props);
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/consul/src/test/java/org/apache/tamaya/consul/ConsulWriteTest.java
----------------------------------------------------------------------
diff --git a/modules/consul/src/test/java/org/apache/tamaya/consul/ConsulWriteTest.java b/modules/consul/src/test/java/org/apache/tamaya/consul/ConsulWriteTest.java
new file mode 100644
index 0000000..1746030
--- /dev/null
+++ b/modules/consul/src/test/java/org/apache/tamaya/consul/ConsulWriteTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.tamaya.consul;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.tamaya.mutableconfig.ConfigChangeRequest;
+import org.apache.tamaya.spi.PropertyValue;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests for the consul backend integration for writing to the consul backend.
+ */
+public class ConsulWriteTest {
+
+ /**
+ * Needs to be enabled manually in case you want to do integration tests.
+ */
+ static boolean execute = false;
+ private static ConsulPropertySource propertySource;
+
+ @BeforeClass
+ public static void setup() throws MalformedURLException, URISyntaxException {
+ System.setProperty("consul.urls", "http://127.0.0.1:8300");
+ propertySource = new ConsulPropertySource();
+
+ System.out.println("At the moment no write-tests can be executed to verify the Consul integration. You can manually edit this test class.");
+ }
+
+ @Test
+ public void testSetNormal() throws Exception {
+ if (!execute) return;
+ String taID = UUID.randomUUID().toString();
+ ConfigChangeRequest request = new ConfigChangeRequest("testSetNormal");
+ request.put(taID, "testSetNormal");
+ propertySource.applyChange(request);
+ }
+
+
+ @Test
+ public void testDelete() throws Exception {
+ if(!execute)return;
+ String taID = UUID.randomUUID().toString();
+ ConfigChangeRequest request = new ConfigChangeRequest("testDelete");
+ request.put(taID, "testDelete");
+ propertySource.applyChange(request);
+ assertEquals(taID.toString(), propertySource.get("testDelete").getValue());
+ assertNotNull(propertySource.get("_testDelete.createdIndex"));
+ request = new ConfigChangeRequest("testDelete2");
+ request.remove("testDelete");
+ propertySource.applyChange(request);
+ assertNull(propertySource.get("testDelete"));
+ }
+
+ @Test
+ public void testGetProperties() throws Exception {
+ if(!execute)return;
+ Map<String,PropertyValue> result = propertySource.getProperties();
+ assertTrue(result.isEmpty());
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/etcd/bnd.bnd
----------------------------------------------------------------------
diff --git a/modules/etcd/bnd.bnd b/modules/etcd/bnd.bnd
new file mode 100644
index 0000000..129f3ec
--- /dev/null
+++ b/modules/etcd/bnd.bnd
@@ -0,0 +1,32 @@
+-buildpath: \
+ osgi.annotation; version=6.0.0,\
+ osgi.core; version=6.0,\
+ osgi.cmpn; version=6.0
+
+-testpath: \
+ ${junit}
+
+javac.source: 1.8
+javac.target: 1.8
+
+Automatic-Module-Name: org.apache.tamaya.etcd
+Bundle-Version: ${version}.${tstamp}
+Bundle-Name: Apache Tamaya - Etcd Config
+Bundle-SymbolicName: org.apache.tamaya.etcd
+Bundle-Description: Apacha Tamaya Config - Etcd PropertySource
+Bundle-Category: Implementation
+Bundle-Copyright: (C) Apache Foundation
+Bundle-License: Apache Licence version 2
+Bundle-Vendor: Apache Software Foundation
+Bundle-ContactAddress: dev-tamaya@incubator.apache.org
+Bundle-DocURL: http://tamaya.apache.org
+Export-Package: \
+ org.apache.tamaya.etcd
+Import-Package: \
+ org.apache.tamaya,\
+ org.apache.tamaya.spi,\
+ org.apache.tamaya.mutableconfig\
+ org.apache.tamaya.mutableconfig.spi
+Export-Service: \
+ org.apache.tamaya.spi.PropertySource
+
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/etcd/pom.xml
----------------------------------------------------------------------
diff --git a/modules/etcd/pom.xml b/modules/etcd/pom.xml
new file mode 100644
index 0000000..f8a9361
--- /dev/null
+++ b/modules/etcd/pom.xml
@@ -0,0 +1,80 @@
+<!--
+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 current 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.tamaya.ext</groupId>
+ <artifactId>tamaya-sandbox</artifactId>
+ <version>0.4-incubating-SNAPSHOT</version>
+ <relativePath>..</relativePath>
+ </parent>
+
+ <artifactId>tamaya-etcd</artifactId>
+ <name>Apache Tamaya Modules - etcd PropertySource</name>
+ <packaging>jar</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>java-hamcrest</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tamaya</groupId>
+ <artifactId>tamaya-core</artifactId>
+ <version>${project.parent.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tamaya</groupId>
+ <artifactId>tamaya-api</artifactId>
+ <version>${project.parent.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tamaya.ext</groupId>
+ <artifactId>tamaya-functions</artifactId>
+ <version>${project.parent.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient-osgi</artifactId>
+ <version>4.5.3</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.geronimo.specs</groupId>
+ <artifactId>geronimo-json_1.1_spec</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.johnzon</groupId>
+ <artifactId>johnzon-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tamaya.ext</groupId>
+ <artifactId>tamaya-mutable-config</artifactId>
+ <version>${project.parent.version}</version>
+ <scope>provided</scope>
+ <optional>true</optional>
+ </dependency>
+ </dependencies>
+
+</project>
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/etcd/src/main/java/org/apache/tamaya/etcd/AbstractEtcdPropertySource.java
----------------------------------------------------------------------
diff --git a/modules/etcd/src/main/java/org/apache/tamaya/etcd/AbstractEtcdPropertySource.java b/modules/etcd/src/main/java/org/apache/tamaya/etcd/AbstractEtcdPropertySource.java
new file mode 100644
index 0000000..3388b8e
--- /dev/null
+++ b/modules/etcd/src/main/java/org/apache/tamaya/etcd/AbstractEtcdPropertySource.java
@@ -0,0 +1,267 @@
+/*
+ * 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.tamaya.etcd;
+
+import org.apache.tamaya.mutableconfig.ConfigChangeRequest;
+import org.apache.tamaya.mutableconfig.spi.MutablePropertySource;
+import org.apache.tamaya.spi.PropertyValue;
+import org.apache.tamaya.spisupport.propertysource.BasePropertySource;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Propertysource that is reading configuration from a configured etcd endpoint. Setting
+ * {@code etcd.prefix} as system property maps the etcd based configuration
+ * to this prefix namespace. Etcd servers are configured as {@code etcd.server.urls} system or environment property.
+ * Etcd can be disabled by setting {@code tamaya.etcdprops.disable} either as environment or system property.
+ */
+public abstract class AbstractEtcdPropertySource extends BasePropertySource
+ implements MutablePropertySource{
+
+ private static final Logger LOG = Logger.getLogger(AbstractEtcdPropertySource.class.getName());
+
+ private String directory ="";
+
+ private List<String> servers = new ArrayList<>();
+
+ private List<EtcdAccessor> etcdBackends = new ArrayList<>();
+
+ private Map<String,String> metaData = new HashMap<>();
+
+ private AtomicLong timeoutDuration = new AtomicLong(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES));
+
+ private AtomicLong timeout = new AtomicLong();
+
+ /** The Hazelcast config map used. */
+ private Map<String, PropertyValue> configMap = new HashMap<>();
+
+ public AbstractEtcdPropertySource(){
+ this("etcd");
+ }
+
+ public AbstractEtcdPropertySource(String name){
+ super(name);
+ metaData.put("source", "etcd");
+ }
+
+ /**
+ * Get the current timeout, when a reload will be triggered on access.
+ * @return the current timeout, or 0 if no data has been loaded at all.
+ */
+ public long getValidUntil(){
+ return timeout.get();
+ }
+
+ /**
+ * Get the current cache timeout.
+ * @return the timeout duration after which data will be reloaded.
+ */
+ public long getCachePeriod(){
+ return timeoutDuration.get();
+ }
+
+ /**
+ * Set the duration after which the data cache will be reloaded.
+ * @param millis the millis
+ */
+ public void setCacheTimeout(long millis){
+ this.timeoutDuration.set(millis);
+ }
+
+ /**
+ * Get the etc directora accessed.
+ * @return the etc director, not null.
+ */
+ public String getDirectory() {
+ return directory;
+ }
+
+ /**
+ * Sets the etcd directory to read from.
+ * @param directory the directory, not null.
+ */
+ public void setDirectory(String directory) {
+ if(!Objects.equals(this.directory, directory)) {
+ this.directory = Objects.requireNonNull(directory);
+ refresh();
+ }
+ }
+
+ public void setServer(List<String> servers) {
+ if(!Objects.equals(this.servers, servers)) {
+ List<EtcdAccessor> etcdBackends = new ArrayList<>();
+ for (String s : servers) {
+ etcdBackends.add(new EtcdAccessor(s));
+ }
+ this.servers = Collections.unmodifiableList(servers);
+ this.etcdBackends = etcdBackends;
+ metaData.put("backends", servers.toString());
+ refresh();
+ }
+ }
+
+ /**
+ * Get the underlying servers this instance will try to connect to.
+ * @return the server list, not null.
+ */
+ public List<String> getServer(){
+ return servers;
+ }
+
+ /**
+ * Checks for a cache timeout and optionally reloads the data.
+ */
+ public void checkRefresh(){
+ if(this.timeout.get() < System.currentTimeMillis()){
+ refresh();
+ }
+ }
+
+ /**
+ * Reloads the data and updated the cache timeouts.
+ */
+ public void refresh() {
+ for(EtcdAccessor accessor: this.etcdBackends){
+ try{
+ Map<String, String> props = accessor.getProperties(directory);
+ if(!props.containsKey("_ERROR")) {
+ this.configMap = mapPrefix(props);
+ this.timeout.set(System.currentTimeMillis() + timeoutDuration.get());
+ } else{
+ LOG.log(Level.FINE, "etcd error on " + accessor.getUrl() + ": " + props.get("_ERROR"));
+ }
+ } catch(Exception e){
+ LOG.log(Level.FINE, "etcd access failed on " + accessor.getUrl() + ", trying next...", e);
+ }
+ }
+ }
+
+ @Override
+ public int getOrdinal() {
+ PropertyValue configuredOrdinal = get(TAMAYA_ORDINAL);
+ if(configuredOrdinal!=null){
+ try{
+ return Integer.parseInt(configuredOrdinal.getValue());
+ } catch(Exception e){
+ Logger.getLogger(getClass().getName()).log(Level.WARNING,
+ "Configured ordinal is not an int number: " + configuredOrdinal, e);
+ }
+ }
+ return getDefaultOrdinal();
+ }
+
+ @Override
+ public PropertyValue get(String key) {
+ checkRefresh();
+ return configMap.get(key);
+ }
+
+ @Override
+ public Map<String, PropertyValue> getProperties() {
+ checkRefresh();
+ return configMap;
+ }
+
+ private Map<String, PropertyValue> mapPrefix(Map<String, String> props) {
+
+ Map<String, PropertyValue> values = new HashMap<>();
+ // Evaluate keys
+ for(Map.Entry<String,String> entry:props.entrySet()) {
+ if (!entry.getKey().startsWith("_")) {
+ PropertyValue val = values.get(entry.getKey());
+ if (val == null) {
+ val = PropertyValue.createValue(entry.getKey(), "").setMeta("source", getName()).setMeta(metaData);
+ values.put(entry.getKey(), val);
+ }
+ }
+ }
+ // add getMeta entries
+ for(Map.Entry<String,String> entry:props.entrySet()) {
+ if (entry.getKey().startsWith("_")) {
+ String key = entry.getKey().substring(1);
+ for(String field:new String[]{".createdIndex", ".modifiedIndex", ".ttl",
+ ".expiration", ".source"}) {
+ if (key.endsWith(field)) {
+ key = key.substring(0, key.length() - field.length());
+ PropertyValue val = values.get(key);
+ if (val != null) {
+ val.setMeta(field, entry.getValue());
+ }
+ }
+ }
+ }
+ }
+ // Map to createValue map.
+// Map<String, PropertyValue> values = new HashMap<>();
+ for(Map.Entry<String,PropertyValue> en:values.entrySet()) {
+ values.put(en.getKey(), en.getValue());
+ }
+ return values;
+ }
+
+ @Override
+ public void applyChange(ConfigChangeRequest configChange) {
+ for(EtcdAccessor accessor: etcdBackends){
+ try{
+ for(String k: configChange.getRemovedProperties()){
+ Map<String,String> res = accessor.delete(k);
+ if(res.get("_ERROR")!=null){
+ LOG.info("Failed to remove key from etcd: " + k);
+ }
+ }
+ for(Map.Entry<String,String> en:configChange.getAddedProperties().entrySet()){
+ String key = en.getKey();
+ Integer ttl = null;
+ int index = en.getKey().indexOf('?');
+ if(index>0){
+ key = en.getKey().substring(0, index);
+ String rawQuery = en.getKey().substring(index+1);
+ String[] queries = rawQuery.split("&");
+ for(String query:queries){
+ if(query.contains("ttl")){
+ int qIdx = query.indexOf('=');
+ ttl = qIdx>0?Integer.parseInt(query.substring(qIdx+1).trim()):null;
+ }
+ }
+ }
+ Map<String,String> res = accessor.set(key, en.getValue(), ttl);
+ if(res.get("_ERROR")!=null){
+ LOG.info("Failed to add key to etcd: " + en.getKey() + "=" + en.getValue());
+ }
+ }
+ // success, stop here
+ break;
+ } catch(Exception e){
+ LOG.log(Level.FINE, "etcd access failed on " + accessor.getUrl() + ", trying next...", e);
+ }
+ }
+ }
+
+
+ @Override
+ protected String toStringValues() {
+ return super.toStringValues() +
+ " directory=" + directory + '\n' +
+ " servers=" + this.servers + '\n';
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/etcd/src/main/java/org/apache/tamaya/etcd/EtcdAccessor.java
----------------------------------------------------------------------
diff --git a/modules/etcd/src/main/java/org/apache/tamaya/etcd/EtcdAccessor.java b/modules/etcd/src/main/java/org/apache/tamaya/etcd/EtcdAccessor.java
new file mode 100644
index 0000000..3317638
--- /dev/null
+++ b/modules/etcd/src/main/java/org/apache/tamaya/etcd/EtcdAccessor.java
@@ -0,0 +1,519 @@
+/*
+ * 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.tamaya.etcd;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.json.JsonReaderFactory;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpStatus;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+
+/**
+ * Accessor for reading to or writing from an etcd endpoint.
+ */
+class EtcdAccessor {
+
+ private static final Logger LOG = Logger.getLogger(EtcdAccessor.class.getName());
+
+ /**
+ * Timeout in seconds.
+ */
+ private int timeout = 2;
+ /**
+ * Timeout in seconds.
+ */
+ private final int socketTimeout = 1000;
+ /**
+ * Timeout in seconds.
+ */
+ private final int connectTimeout = 1000;
+
+ /**
+ * Property that makes Johnzon accept comments.
+ */
+ public static final String JOHNZON_SUPPORTS_COMMENTS_PROP = "org.apache.johnzon.supports-comments";
+ /**
+ * The JSON reader factory used.
+ */
+ private final JsonReaderFactory readerFactory = initReaderFactory();
+
+ /**
+ * Initializes the factory to be used for creating readers.
+ */
+ private JsonReaderFactory initReaderFactory() {
+ final Map<String, Object> config = new HashMap<>();
+ config.put(JOHNZON_SUPPORTS_COMMENTS_PROP, true);
+ return Json.createReaderFactory(config);
+ }
+
+ /**
+ * The base server url.
+ */
+ private final String serverURL;
+ /**
+ * The http client.
+ */
+ private final CloseableHttpClient httpclient = HttpClients.createDefault();
+
+ /**
+ * Creates a new instance with the basic access url.
+ *
+ * @param server server url, e.g. {@code http://127.0.0.1:4001}, not null.
+ */
+ public EtcdAccessor(String server) {
+ this(server, 2);
+ }
+
+ public EtcdAccessor(String server, int timeout) {
+ this.timeout = timeout;
+ if (server.endsWith("/")) {
+ serverURL = server.substring(0, server.length() - 1);
+ } else {
+ serverURL = server;
+ }
+ }
+
+ /**
+ * Get the etcd server version.
+ *
+ * @return the etcd server version, never null.
+ */
+ public String getVersion() {
+ String version = "<ERROR>";
+ try {
+ final CloseableHttpClient httpclient = HttpClients.createDefault();
+ final HttpGet httpGet = new HttpGet(serverURL + "/version");
+ httpGet.setConfig(RequestConfig.copy(RequestConfig.DEFAULT).setSocketTimeout(socketTimeout)
+ .setConnectTimeout(timeout).build());
+ try (CloseableHttpResponse response = httpclient.execute(httpGet)) {
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ final HttpEntity entity = response.getEntity();
+ // and ensure it is fully consumed
+ version = EntityUtils.toString(entity);
+ EntityUtils.consume(entity);
+ }
+ }
+ return version;
+ } catch (final Exception e) {
+ LOG.log(Level.INFO, "Error getting etcd version from: " + serverURL, e);
+ }
+ return version;
+ }
+
+ /**
+ * Ask etcd for a single key, createValue pair. Hereby the response returned from
+ * etcd:
+ *
+ * <pre>
+ * {
+ * "action": "current",
+ * "getField": {
+ * "createdIndex": 2,
+ * "key": "/message",
+ * "modifiedIndex": 2,
+ * "createValue": "Hello world"
+ * }
+ * }
+ * </pre>
+ *
+ * is mapped to:
+ *
+ * <pre>
+ * key=createValue
+ * _key.source=[etcd]http://127.0.0.1:4001
+ * _key.createdIndex=12
+ * _key.modifiedIndex=34
+ * _key.ttl=300
+ * _key.expiration=...
+ * </pre>
+ *
+ * @param key the requested key
+ * @return the mapped result, including getMeta-entries.
+ */
+ public Map<String, String> get(String key) {
+ final Map<String, String> result = new HashMap<>();
+ try {
+ final HttpGet httpGet = new HttpGet(serverURL + "/v2/keys/" + key);
+ httpGet.setConfig(RequestConfig.copy(RequestConfig.DEFAULT).setSocketTimeout(socketTimeout)
+ .setConnectionRequestTimeout(timeout).setConnectTimeout(connectTimeout).build());
+ try (CloseableHttpResponse response = httpclient.execute(httpGet)) {
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ final HttpEntity entity = response.getEntity();
+ final JsonReader reader = readerFactory
+ .createReader(new StringReader(EntityUtils.toString(entity)));
+ final JsonObject o = reader.readObject();
+ final JsonObject node = o.getJsonObject("getField");
+ if (node.containsKey("createValue")) {
+ result.put(key, node.getString("createValue"));
+ result.put("_" + key + ".source", "[etcd]" + serverURL);
+ }
+ if (node.containsKey("createdIndex")) {
+ result.put("_" + key + ".createdIndex", String.valueOf(node.getInt("createdIndex")));
+ }
+ if (node.containsKey("modifiedIndex")) {
+ result.put("_" + key + ".modifiedIndex", String.valueOf(node.getInt("modifiedIndex")));
+ }
+ if (node.containsKey("expiration")) {
+ result.put("_" + key + ".expiration", String.valueOf(node.getString("expiration")));
+ }
+ if (node.containsKey("ttl")) {
+ result.put("_" + key + ".ttl", String.valueOf(node.getInt("ttl")));
+ }
+ EntityUtils.consume(entity);
+ } else {
+ result.put("_" + key + ".NOT_FOUND.target", "[etcd]" + serverURL);
+ }
+ }
+ } catch (final Exception e) {
+ LOG.log(Level.INFO, "Error reading key '" + key + "' from etcd: " + serverURL, e);
+ result.put("_ERROR", "Error reading key '" + key + "' from etcd: " + serverURL + ": " + e.toString());
+ }
+ return result;
+ }
+
+ /**
+ * Creates/updates an entry in etcd without any ttl setCurrent.
+ *
+ * @param key the property key, not null
+ * @param value the createValue to be setCurrent
+ * @return the result map as described above.
+ * @see #set(String, String, Integer)
+ */
+ public Map<String, String> set(String key, String value) {
+ return set(key, value, null);
+ }
+
+ /**
+ * Creates/updates an entry in etcd. The response as follows:
+ *
+ * <pre>
+ * {
+ * "action": "setCurrent",
+ * "getField": {
+ * "createdIndex": 3,
+ * "key": "/message",
+ * "modifiedIndex": 3,
+ * "createValue": "Hello etcd"
+ * },
+ * "prevNode": {
+ * "createdIndex": 2,
+ * "key": "/message",
+ * "createValue": "Hello world",
+ * "modifiedIndex": 2
+ * }
+ * }
+ * </pre>
+ *
+ * is mapped to:
+ *
+ * <pre>
+ * key=createValue
+ * _key.source=[etcd]http://127.0.0.1:4001
+ * _key.createdIndex=12
+ * _key.modifiedIndex=34
+ * _key.ttl=300
+ * _key.expiry=...
+ * // optional
+ * _key.prevNode.createdIndex=12
+ * _key.prevNode.modifiedIndex=34
+ * _key.prevNode.ttl=300
+ * _key.prevNode.expiration=...
+ * </pre>
+ *
+ * @param key the property key, not null
+ * @param value the createValue to be setCurrent
+ * @param ttlSeconds the ttl in seconds (optional)
+ * @return the result map as described above.
+ */
+ public Map<String, String> set(String key, String value, Integer ttlSeconds) {
+ final Map<String, String> result = new HashMap<>();
+ try {
+ final HttpPut put = new HttpPut(serverURL + "/v2/keys/" + key);
+ put.setConfig(RequestConfig.copy(RequestConfig.DEFAULT).setSocketTimeout(socketTimeout)
+ .setConnectionRequestTimeout(timeout).setConnectTimeout(connectTimeout).build());
+ final List<NameValuePair> nvps = new ArrayList<>();
+ nvps.add(new BasicNameValuePair("createValue", value));
+ if (ttlSeconds != null) {
+ nvps.add(new BasicNameValuePair("ttl", ttlSeconds.toString()));
+ }
+ put.setEntity(new UrlEncodedFormEntity(nvps));
+ try (CloseableHttpResponse response = httpclient.execute(put)) {
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED
+ || response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ final HttpEntity entity = response.getEntity();
+ final JsonReader reader = readerFactory
+ .createReader(new StringReader(EntityUtils.toString(entity)));
+ final JsonObject o = reader.readObject();
+ final JsonObject node = o.getJsonObject("getField");
+ if (node.containsKey("createdIndex")) {
+ result.put("_" + key + ".createdIndex", String.valueOf(node.getInt("createdIndex")));
+ }
+ if (node.containsKey("modifiedIndex")) {
+ result.put("_" + key + ".modifiedIndex", String.valueOf(node.getInt("modifiedIndex")));
+ }
+ if (node.containsKey("expiration")) {
+ result.put("_" + key + ".expiration", String.valueOf(node.getString("expiration")));
+ }
+ if (node.containsKey("ttl")) {
+ result.put("_" + key + ".ttl", String.valueOf(node.getInt("ttl")));
+ }
+ result.put(key, node.getString("createValue"));
+ result.put("_" + key + ".source", "[etcd]" + serverURL);
+ parsePrevNode(key, result, node);
+ EntityUtils.consume(entity);
+ }
+ }
+ } catch (final Exception e) {
+ LOG.log(Level.INFO, "Error writing to etcd: " + serverURL, e);
+ result.put("_ERROR", "Error writing '" + key + "' to etcd: " + serverURL + ": " + e.toString());
+ }
+ return result;
+ }
+
+ /**
+ * Deletes a given key. The response is as follows:
+ *
+ * <pre>
+ * _key.source=[etcd]http://127.0.0.1:4001
+ * _key.createdIndex=12
+ * _key.modifiedIndex=34
+ * _key.ttl=300
+ * _key.expiry=...
+ * // optional
+ * _key.prevNode.createdIndex=12
+ * _key.prevNode.modifiedIndex=34
+ * _key.prevNode.ttl=300
+ * _key.prevNode.expiration=...
+ * _key.prevNode.createValue=...
+ * </pre>
+ *
+ * @param key the key to be deleted.
+ * @return the response maps as described above.
+ */
+ public Map<String, String> delete(String key) {
+ final Map<String, String> result = new HashMap<>();
+ try {
+ final HttpDelete delete = new HttpDelete(serverURL + "/v2/keys/" + key);
+ delete.setConfig(RequestConfig.copy(RequestConfig.DEFAULT).setSocketTimeout(socketTimeout)
+ .setConnectionRequestTimeout(timeout).setConnectTimeout(connectTimeout).build());
+ try (CloseableHttpResponse response = httpclient.execute(delete)) {
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ final HttpEntity entity = response.getEntity();
+ final JsonReader reader = readerFactory
+ .createReader(new StringReader(EntityUtils.toString(entity)));
+ final JsonObject o = reader.readObject();
+ final JsonObject node = o.getJsonObject("getField");
+ if (node.containsKey("createdIndex")) {
+ result.put("_" + key + ".createdIndex", String.valueOf(node.getInt("createdIndex")));
+ }
+ if (node.containsKey("modifiedIndex")) {
+ result.put("_" + key + ".modifiedIndex", String.valueOf(node.getInt("modifiedIndex")));
+ }
+ if (node.containsKey("expiration")) {
+ result.put("_" + key + ".expiration", String.valueOf(node.getString("expiration")));
+ }
+ if (node.containsKey("ttl")) {
+ result.put("_" + key + ".ttl", String.valueOf(node.getInt("ttl")));
+ }
+ parsePrevNode(key, result, o);
+ EntityUtils.consume(entity);
+ }
+ }
+ } catch (final Exception e) {
+ LOG.log(Level.INFO, "Error deleting key '" + key + "' from etcd: " + serverURL, e);
+ result.put("_ERROR", "Error deleting '" + key + "' from etcd: " + serverURL + ": " + e.toString());
+ }
+ return result;
+ }
+
+ private static void parsePrevNode(String key, Map<String, String> result, JsonObject o) {
+ if (o.containsKey("prevNode")) {
+ final JsonObject prevNode = o.getJsonObject("prevNode");
+ if (prevNode.containsKey("createdIndex")) {
+ result.put("_" + key + ".prevNode.createdIndex",
+ String.valueOf(prevNode.getInt("createdIndex")));
+ }
+ if (prevNode.containsKey("modifiedIndex")) {
+ result.put("_" + key + ".prevNode.modifiedIndex",
+ String.valueOf(prevNode.getInt("modifiedIndex")));
+ }
+ if (prevNode.containsKey("expiration")) {
+ result.put("_" + key + ".prevNode.expiration",
+ String.valueOf(prevNode.getString("expiration")));
+ }
+ if (prevNode.containsKey("ttl")) {
+ result.put("_" + key + ".prevNode.ttl", String.valueOf(prevNode.getInt("ttl")));
+ }
+ result.put("_" + key + ".prevNode.createValue", prevNode.getString("createValue"));
+ }
+ }
+
+ /**
+ * Get all properties for the given directory key recursively.
+ *
+ * @param directory the directory entry
+ * @return the properties and its metadata
+ * @see #getProperties(String, boolean)
+ */
+ public Map<String, String> getProperties(String directory) {
+ return getProperties(directory, true);
+ }
+
+ /**
+ * Access all properties. The response of:
+ *
+ * <pre>
+ * {
+ * "action": "current",
+ * "getField": {
+ * "key": "/",
+ * "dir": true,
+ * "getList": [
+ * {
+ * "key": "/foo_dir",
+ * "dir": true,
+ * "modifiedIndex": 2,
+ * "createdIndex": 2
+ * },
+ * {
+ * "key": "/foo",
+ * "createValue": "two",
+ * "modifiedIndex": 1,
+ * "createdIndex": 1
+ * }
+ * ]
+ * }
+ * }
+ * </pre>
+ *
+ * is mapped to a regular Tamaya properties map as follows:
+ *
+ * <pre>
+ * key1=myvalue
+ * _key1.source=[etcd]http://127.0.0.1:4001
+ * _key1.createdIndex=12
+ * _key1.modifiedIndex=34
+ * _key1.ttl=300
+ * _key1.expiration=...
+ *
+ * key2=myvaluexxx
+ * _key2.source=[etcd]http://127.0.0.1:4001
+ * _key2.createdIndex=12
+ *
+ * key3=val3
+ * _key3.source=[etcd]http://127.0.0.1:4001
+ * _key3.createdIndex=12
+ * _key3.modifiedIndex=2
+ * </pre>
+ *
+ * @param directory remote directory to query.
+ * @param recursive allows to setCurrent if querying is performed recursively
+ * @return all properties read from the remote server.
+ */
+ public Map<String, String> getProperties(String directory, boolean recursive) {
+ final Map<String, String> result = new HashMap<>();
+ try {
+ final HttpGet get = new HttpGet(serverURL + "/v2/keys/" + directory + "?recursive=" + recursive);
+ get.setConfig(RequestConfig.copy(RequestConfig.DEFAULT).setSocketTimeout(socketTimeout)
+ .setConnectionRequestTimeout(timeout).setConnectTimeout(connectTimeout).build());
+ try (CloseableHttpResponse response = httpclient.execute(get)) {
+
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ final HttpEntity entity = response.getEntity();
+ final JsonReader reader = readerFactory.createReader(new StringReader(EntityUtils.toString(entity)));
+ final JsonObject o = reader.readObject();
+ final JsonObject node = o.getJsonObject("getField");
+ if (node != null) {
+ addNodes(result, node);
+ }
+ EntityUtils.consume(entity);
+ }
+ }
+ } catch (final Exception e) {
+ LOG.log(Level.INFO, "Error reading properties for '" + directory + "' from etcd: " + serverURL, e);
+ result.put("_ERROR",
+ "Error reading properties for '" + directory + "' from etcd: " + serverURL + ": " + e.toString());
+ }
+ return result;
+ }
+
+ /**
+ * Recursively read out all key/values from this etcd JSON array.
+ *
+ * @param result map with key, values and metadata.
+ * @param node the getField to parse.
+ */
+ private void addNodes(Map<String, String> result, JsonObject node) {
+ if (!node.containsKey("dir") || "false".equals(node.get("dir").toString())) {
+ final String key = node.getString("key").substring(1);
+ result.put(key, node.getString("createValue"));
+ if (node.containsKey("createdIndex")) {
+ result.put("_" + key + ".createdIndex", String.valueOf(node.getInt("createdIndex")));
+ }
+ if (node.containsKey("modifiedIndex")) {
+ result.put("_" + key + ".modifiedIndex", String.valueOf(node.getInt("modifiedIndex")));
+ }
+ if (node.containsKey("expiration")) {
+ result.put("_" + key + ".expiration", String.valueOf(node.getString("expiration")));
+ }
+ if (node.containsKey("ttl")) {
+ result.put("_" + key + ".ttl", String.valueOf(node.getInt("ttl")));
+ }
+ result.put("_" + key + ".source", "[etcd]" + serverURL);
+ } else {
+ final JsonArray nodes = node.getJsonArray("getList");
+ if (nodes != null) {
+ for (int i = 0; i < nodes.size(); i++) {
+ addNodes(result, nodes.getJsonObject(i));
+ }
+ }
+ }
+ }
+
+ /**
+ * Access the server root URL used by this accessor.
+ *
+ * @return the server root URL.
+ */
+ public String getUrl() {
+ return serverURL;
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/etcd/src/main/java/org/apache/tamaya/etcd/EtcdBackendConfig.java
----------------------------------------------------------------------
diff --git a/modules/etcd/src/main/java/org/apache/tamaya/etcd/EtcdBackendConfig.java b/modules/etcd/src/main/java/org/apache/tamaya/etcd/EtcdBackendConfig.java
new file mode 100644
index 0000000..95561c9
--- /dev/null
+++ b/modules/etcd/src/main/java/org/apache/tamaya/etcd/EtcdBackendConfig.java
@@ -0,0 +1,95 @@
+/*
+ * 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.tamaya.etcd;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Singleton that reads the current etcd setup, especially the possible URLs to be used.
+ */
+final class EtcdBackendConfig {
+
+ private static final Logger LOG = Logger.getLogger(EtcdBackendConfig.class.getName());
+ private static final String TAMAYA_ETCD_SERVER_URLS = "tamaya.etcd.server";
+ private static final String TAMAYA_ETCD_TIMEOUT = "tamaya.etcd.timeout";
+ private static final String TAMAYA_ETCD_DIRECTORY = "tamaya.etcd.directory";
+
+
+ private EtcdBackendConfig(){}
+
+ /**
+ * Get the default etcd directory selector, default {@code ""}.
+ * @return the default etcd directory selector, never null.
+ */
+ public static String getEtcdDirectory(){
+ String val = System.getProperty(TAMAYA_ETCD_DIRECTORY);
+ if(val == null){
+ val = System.getenv(TAMAYA_ETCD_DIRECTORY);
+ }
+ if(val!=null){
+ return val;
+ }
+ return "";
+ }
+
+ /**
+ * Get the etcd connection timeout from system/enfironment property {@code tamaya.etcd.timeout (=seconds)}
+ * (default 2 seconds).
+ * @return the etcd connection timeout.
+ */
+ public static long getEtcdTimeout(){
+ String val = System.getProperty(TAMAYA_ETCD_TIMEOUT);
+ if(val == null){
+ val = System.getenv(TAMAYA_ETCD_TIMEOUT);
+ }
+ if(val!=null){
+ return TimeUnit.MILLISECONDS.convert(Integer.parseInt(val), TimeUnit.SECONDS);
+ }
+ return 2000L;
+ }
+
+ /**
+ * Evaluate the etcd target servers fomr system/environment property {@code tamaya.etcd.server}.
+ * @return the servers configured, or {@code http://127.0.0.1:4001} (default).
+ */
+ public static List<String> getServers(){
+ String serverURLs = System.getProperty(TAMAYA_ETCD_SERVER_URLS);
+ if(serverURLs==null){
+ serverURLs = System.getenv(TAMAYA_ETCD_SERVER_URLS);
+ }
+ if(serverURLs==null){
+ serverURLs = "http://127.0.0.1:4001";
+ }
+ List<String> servers = new ArrayList<>();
+ for(String url:serverURLs.split("\\,")) {
+ try{
+ servers.add(url.trim());
+ LOG.info("Using etcd endoint: " + url);
+ } catch(Exception e){
+ LOG.log(Level.SEVERE, "Error initializing etcd accessor for URL: " + url, e);
+ }
+ }
+ return servers;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-tamaya-extensions/blob/c8ba9c4c/modules/etcd/src/main/java/org/apache/tamaya/etcd/EtcdPropertySource.java
----------------------------------------------------------------------
diff --git a/modules/etcd/src/main/java/org/apache/tamaya/etcd/EtcdPropertySource.java b/modules/etcd/src/main/java/org/apache/tamaya/etcd/EtcdPropertySource.java
new file mode 100644
index 0000000..d168ab0
--- /dev/null
+++ b/modules/etcd/src/main/java/org/apache/tamaya/etcd/EtcdPropertySource.java
@@ -0,0 +1,52 @@
+/*
+ * 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.tamaya.etcd;
+
+import org.apache.tamaya.spi.PropertyValue;
+
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * Propertysource that is reading configuration from a configured etcd endpoint. Setting
+ * {@code etcd.prefix} as system property maps the etcd based configuration
+ * to this prefix namespace. Etcd servers are configured as {@code etcd.server.urls} system or environment property.
+ * Etcd can be disabled by setting {@code tamaya.etcdprops.disable} either as environment or system property.
+ */
+public class EtcdPropertySource extends AbstractEtcdPropertySource{
+
+ private static final Logger LOG = Logger.getLogger(EtcdPropertySource.class.getName());
+
+ public EtcdPropertySource(List<String> server){
+ this();
+ setServer(server);
+ }
+
+ public EtcdPropertySource(String... server){
+ this();
+ setServer(Arrays.asList(server));
+ }
+
+ public EtcdPropertySource(){
+ setDefaultOrdinal(1000);
+ setDirectory(EtcdBackendConfig.getEtcdDirectory());
+ setServer(EtcdBackendConfig.getServers());
+ }
+
+}