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());
+    }
+
+}