diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySource.java b/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySource.java index b3fa901a..1c088132 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySource.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySource.java @@ -28,6 +28,7 @@ import org.springframework.util.Assert; import org.springframework.vault.client.VaultException; import org.springframework.vault.core.VaultOperations; import org.springframework.vault.core.VaultTemplate; +import org.springframework.vault.support.JsonMapFlattener; import org.springframework.vault.support.VaultResponse; /** @@ -40,10 +41,12 @@ import org.springframework.vault.support.VaultResponse; */ public class VaultPropertySource extends EnumerablePropertySource { - protected final static Log logger = LogFactory.getLog(VaultPropertySource.class); + private final static Log logger = LogFactory.getLog(VaultPropertySource.class); private final String path; + private final Map properties = new LinkedHashMap(); + private final Object lock = new Object(); /** @@ -129,19 +132,7 @@ public class VaultPropertySource extends EnumerablePropertySource toStringMap(Map data) { - - Map result = new LinkedHashMap(); - - if (data != null) { - for (String s : data.keySet()) { - Object value = data.get(s); - if (value != null) { - result.put(s, value.toString()); - } - } - } - - return result; + return JsonMapFlattener.flatten(data); } @Override diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/JsonMapFlattener.java b/spring-vault-core/src/main/java/org/springframework/vault/support/JsonMapFlattener.java new file mode 100644 index 00000000..e8c6558a --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/JsonMapFlattener.java @@ -0,0 +1,122 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.support; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Flattens a hierarchical {@link Map} of objects into a property {@link Map}. + *

+ * Flattening is particularly useful when representing a JSON object as + * {@link java.util.Properties} + *

+ * {@link JsonMapFlattener} flattens {@link Map maps} containing nested + * {@link java.util.List}, {@link Map} and simple values into a flat representation. The + * hierarchical structure is reflected in properties using dot-notation. Nested maps are + * considered as sub-documents. + *

+ * Input: + * + *

+ * 
+ *     {"key": {"nested: "value"}, "another.key": ["one", "two"] }
+ * 
+ * 
+ * + *
+ * Result + * + *
+ *  key.nested=value
+ *  another.key[0]=one
+ *  another.key[1]=two
+ * 
+ * 
+ * + * @author Mark Paluch + */ +public abstract class JsonMapFlattener { + + private JsonMapFlattener() { + } + + /** + * Flatten a hierarchical {@link Map} into a flat {@link Map} with key names using + * property dot notation. + * + * @param inputMap must not be {@literal null}. + * @return the resulting {@link Map}. + */ + public static Map flatten(Map inputMap) { + + Assert.notNull(inputMap, "Input Map must not be null"); + + Map resultMap = new LinkedHashMap(); + + doFlatten("", inputMap.entrySet().iterator(), resultMap); + + return resultMap; + } + + private static void doFlatten(String propertyPrefix, + Iterator> inputMap, Map resultMap) { + + if (StringUtils.hasText(propertyPrefix)) { + propertyPrefix = propertyPrefix + "."; + } + + while (inputMap.hasNext()) { + + Entry entry = inputMap.next(); + flattenElement(propertyPrefix + entry.getKey(), entry.getValue(), resultMap); + } + } + + @SuppressWarnings("unchecked") + private static void flattenElement(String propertyPrefix, Object source, + Map resultMap) { + + if (source instanceof Iterable) { + flattenCollection(propertyPrefix, (Iterable) source, resultMap); + return; + } + + if (source instanceof Map) { + doFlatten(propertyPrefix, ((Map) source).entrySet().iterator(), + resultMap); + return; + } + + resultMap.put(propertyPrefix, source == null ? null : source.toString()); + } + + private static void flattenCollection(String propertyPrefix, + Iterable iterable, Map resultMap) { + + int counter = 0; + + for (Object element : iterable) { + flattenElement(propertyPrefix + "[" + counter + "]", element, resultMap); + counter++; + } + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateGenericIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateGenericIntegrationTests.java index 215c0551..1af69c73 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateGenericIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateGenericIntegrationTests.java @@ -15,15 +15,16 @@ */ package org.springframework.vault.core; -import static org.assertj.core.api.Assertions.*; - +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; @@ -31,6 +32,8 @@ import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultResponseSupport; import org.springframework.vault.util.IntegrationTestSupport; +import static org.assertj.core.api.Assertions.assertThat; + /** * Integration tests for {@link VaultTemplate} using the {@code generic} backend. * @@ -61,6 +64,37 @@ public class VaultTemplateGenericIntegrationTests extends IntegrationTestSupport assertThat(read.getData()).containsEntry("hello", "world"); } + @Test + public void readShouldReturnNestedPropertiesKey() throws Exception { + + Map map = new ObjectMapper() + .readValue( + "{ \"hello.array[0]\":\"array-value0\", \"hello.array[1]\":\"array-value1\" }", + Map.class); + vaultOperations.write("secret/mykey", map); + + VaultResponse read = vaultOperations.read("secret/mykey"); + assertThat(read).isNotNull(); + assertThat(read.getData()).containsEntry("hello.array[0]", "array-value0"); + assertThat(read.getData()).containsEntry("hello.array[1]", "array-value1"); + } + + @Test + public void readShouldReturnNestedObjects() throws Exception { + + Map map = new ObjectMapper().readValue( + "{ \"array\": [ {\"hello\": \"world\"}, {\"hello1\": \"world1\"} ] }", + Map.class); + vaultOperations.write("secret/mykey", map); + + VaultResponse read = vaultOperations.read("secret/mykey"); + assertThat(read).isNotNull(); + assertThat(read.getData()).containsEntry( + "array", + Arrays.asList(Collections.singletonMap("hello", "world"), + Collections.singletonMap("hello1", "world1"))); + } + @Test public void readObjectShouldReadDomainClass() throws Exception { diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/env/VaultPropertySourceUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/env/VaultPropertySourceUnitTests.java index 6b17efb1..3a717c50 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/env/VaultPropertySourceUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/env/VaultPropertySourceUnitTests.java @@ -15,6 +15,7 @@ */ package org.springframework.vault.core.env; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -61,6 +62,7 @@ public class VaultPropertySourceUnitTests { assertThat(vaultPropertySource.getProperty("key")).isEqualTo("value"); assertThat(vaultPropertySource.getProperty("integer")).isEqualTo("1"); + assertThat(vaultPropertySource.getProperty("complex.key")).isEqualTo("value"); } @Test @@ -71,7 +73,8 @@ public class VaultPropertySourceUnitTests { VaultPropertySource vaultPropertySource = new VaultPropertySource("hello", vaultTemplate, "secret/myapp"); - assertThat(vaultPropertySource.getPropertyNames()).contains("key", "integer"); + assertThat(vaultPropertySource.getPropertyNames()).contains("key", "integer", + "complex.key"); } private void prepareResponse() { @@ -79,6 +82,7 @@ public class VaultPropertySourceUnitTests { Map data = new LinkedHashMap(); data.put("key", "value"); data.put("integer", 1); + data.put("complex", Collections.singletonMap("key", "value")); VaultResponse vaultResponse = new VaultResponse(); vaultResponse.setData(data); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/support/JsonMapFlattenerUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/support/JsonMapFlattenerUnitTests.java new file mode 100644 index 00000000..540701a8 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/support/JsonMapFlattenerUnitTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.support; + +import java.util.Collections; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link JsonMapFlattener}. + * + * @author Mark Paluch + */ +@SuppressWarnings("unchecked") +public class JsonMapFlattenerUnitTests { + + private final static ObjectMapper objectMapper = new ObjectMapper(); + + @Test + public void shouldPreserveFlatMap() throws Exception { + + Map result = JsonMapFlattener.flatten(Collections.singletonMap( + "key", "value")); + assertThat(result).containsEntry("key", "value"); + } + + @Test + public void shouldFlattenNestedObject() throws Exception { + + Map map = objectMapper.readValue( + "{\"key\": { \"nested\":\"value\"} }", Map.class); + Map result = JsonMapFlattener.flatten(map); + + assertThat(result).containsEntry("key.nested", "value"); + } + + @Test + public void shouldFlattenDeeplyNestedObject() throws Exception { + + Map map = objectMapper.readValue( + "{\"key\": { \"nested\": {\"anotherLevel\": \"value\"} } }", Map.class); + Map result = JsonMapFlattener.flatten(map); + + assertThat(result).containsEntry("key.nested.anotherLevel", "value"); + } + + @Test + public void shouldFlattenNestedListOfSimpleObjects() throws Exception { + + Map map = objectMapper.readValue( + "{\"key\": [\"one\", \"two\"], \"dotted.key\": [\"one\", \"two\"] }", + Map.class); + Map result = JsonMapFlattener.flatten(map); + + assertThat(result).containsEntry("key[0]", "one").containsEntry("key[1]", "two"); + assertThat(result).containsEntry("dotted.key[0]", "one").containsEntry( + "dotted.key[1]", "two"); + } + + @Test + public void shouldFlattenNestedListOfComplexObject() throws Exception { + + Map map = objectMapper.readValue( + "{\"key\": [{ \"nested\":\"value\"}, { \"nested\":\"other-value\"}] }", + Map.class); + Map result = JsonMapFlattener.flatten(map); + + assertThat(result).containsEntry("key[0].nested", "value").containsEntry( + "key[1].nested", "other-value"); + } + + @Test + public void shouldFlattenDeeplyNestedListOfComplexObject() throws Exception { + + Map map = objectMapper + .readValue( + "{\"key\": { \"level1\": [{ \"nested\":\"value\"}, { \"nested\":\"other-value\"}]} }", + Map.class); + Map result = JsonMapFlattener.flatten(map); + + assertThat(result).containsEntry("key.level1[0].nested", "value").containsEntry( + "key.level1[1].nested", "other-value"); + } +} \ No newline at end of file diff --git a/src/main/asciidoc/reference/getting-started.adoc b/src/main/asciidoc/reference/getting-started.adoc index ed60a844..82263c20 100644 --- a/src/main/asciidoc/reference/getting-started.adoc +++ b/src/main/asciidoc/reference/getting-started.adoc @@ -347,16 +347,32 @@ manipulation of the set of property sources. === @VaultPropertySource The `@VaultPropertySource` annotation provides a convenient and declarative -mechanism for adding a `PropertySource` to Spring’s `Environment`. +mechanism for adding a `PropertySource` to Spring's `Environment` +to be used in conjunction with @Configuration classes. -To be used in conjunction with @Configuration classes. -Example usage +`@VaultPropertySource` takes a Vault path such as ``secret/my-application`` +and exposes the data stored at the node in a ``PropertySource``. -Given a Vault path `secret/my-application` containing the configuration data -pair `database.password=mysecretpassword`, the following `@Configuration` -class uses `@VaultPropertySource` to contribute `secret/my-application` to -the `Environment`'s set of `PropertySources`. +.Properties stored in Vault +==== +[source,json] +---- +{ + // … + "data": { + "database": { + "password": ... + }, + "user.name": ..., + } + + // … +} +---- +==== + +.Declaring a `@VaultPropertySource` ==== [source,java] ---- @@ -369,6 +385,7 @@ public class AppConfig { @Bean public TestBean testBean() { TestBean testBean = new TestBean(); + testBean.setUser(env.getProperty("user.name")); testBean.setPassword(env.getProperty("database.password")); return testBean; } @@ -378,7 +395,7 @@ public class AppConfig { In certain situations, it may not be possible or practical to tightly control property source ordering when using `@VaultPropertySource` annotations. -For example, if the @Configuration classes above were registered via +For example, if the `@Configuration` classes above were registered via component-scanning, the ordering is difficult to predict. In such cases - and if overriding is important - it is recommended that the user fall back to using the programmatic PropertySource API.