Flatten hierarchical JSON objects into property paths.
Hierarchical JSON data stored in Vault is flattened to property paths with dot-notation.
{
"database": {
"password": ...
},
"items": ["one", "two"],
"user.name": ...,
}
results in
database.password=...
items[0]=one
items[1]=two
user.name=...
Fixes gh-40.
This commit is contained in:
@@ -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<VaultOperations> {
|
||||
|
||||
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<String, String> properties = new LinkedHashMap<String, String>();
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
/**
|
||||
@@ -129,19 +132,7 @@ public class VaultPropertySource extends EnumerablePropertySource<VaultOperation
|
||||
* @return
|
||||
*/
|
||||
protected Map<String, String> toStringMap(Map<String, Object> data) {
|
||||
|
||||
Map<String, String> result = new LinkedHashMap<String, String>();
|
||||
|
||||
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
|
||||
|
||||
@@ -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}.
|
||||
* <p>
|
||||
* Flattening is particularly useful when representing a JSON object as
|
||||
* {@link java.util.Properties}
|
||||
* <p>
|
||||
* {@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.
|
||||
* <p>
|
||||
* Input:
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* {"key": {"nested: "value"}, "another.key": ["one", "two"] }
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* <br/>
|
||||
* Result
|
||||
*
|
||||
* <pre>
|
||||
* <code> key.nested=value
|
||||
* another.key[0]=one
|
||||
* another.key[1]=two
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* @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<String, String> flatten(Map<String, ? extends Object> inputMap) {
|
||||
|
||||
Assert.notNull(inputMap, "Input Map must not be null");
|
||||
|
||||
Map<String, String> resultMap = new LinkedHashMap<String, String>();
|
||||
|
||||
doFlatten("", inputMap.entrySet().iterator(), resultMap);
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
private static void doFlatten(String propertyPrefix,
|
||||
Iterator<? extends Entry<String, ?>> inputMap, Map<String, String> resultMap) {
|
||||
|
||||
if (StringUtils.hasText(propertyPrefix)) {
|
||||
propertyPrefix = propertyPrefix + ".";
|
||||
}
|
||||
|
||||
while (inputMap.hasNext()) {
|
||||
|
||||
Entry<String, ? extends Object> entry = inputMap.next();
|
||||
flattenElement(propertyPrefix + entry.getKey(), entry.getValue(), resultMap);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void flattenElement(String propertyPrefix, Object source,
|
||||
Map<String, String> resultMap) {
|
||||
|
||||
if (source instanceof Iterable) {
|
||||
flattenCollection(propertyPrefix, (Iterable<Object>) source, resultMap);
|
||||
return;
|
||||
}
|
||||
|
||||
if (source instanceof Map) {
|
||||
doFlatten(propertyPrefix, ((Map<String, ?>) source).entrySet().iterator(),
|
||||
resultMap);
|
||||
return;
|
||||
}
|
||||
|
||||
resultMap.put(propertyPrefix, source == null ? null : source.toString());
|
||||
}
|
||||
|
||||
private static void flattenCollection(String propertyPrefix,
|
||||
Iterable<Object> iterable, Map<String, String> resultMap) {
|
||||
|
||||
int counter = 0;
|
||||
|
||||
for (Object element : iterable) {
|
||||
flattenElement(propertyPrefix + "[" + counter + "]", element, resultMap);
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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<String, Object> data = new LinkedHashMap<String, Object>();
|
||||
data.put("key", "value");
|
||||
data.put("integer", 1);
|
||||
data.put("complex", Collections.singletonMap("key", "value"));
|
||||
|
||||
VaultResponse vaultResponse = new VaultResponse();
|
||||
vaultResponse.setData(data);
|
||||
|
||||
@@ -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<String, String> result = JsonMapFlattener.flatten(Collections.singletonMap(
|
||||
"key", "value"));
|
||||
assertThat(result).containsEntry("key", "value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFlattenNestedObject() throws Exception {
|
||||
|
||||
Map<String, Object> map = objectMapper.readValue(
|
||||
"{\"key\": { \"nested\":\"value\"} }", Map.class);
|
||||
Map<String, String> result = JsonMapFlattener.flatten(map);
|
||||
|
||||
assertThat(result).containsEntry("key.nested", "value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFlattenDeeplyNestedObject() throws Exception {
|
||||
|
||||
Map<String, Object> map = objectMapper.readValue(
|
||||
"{\"key\": { \"nested\": {\"anotherLevel\": \"value\"} } }", Map.class);
|
||||
Map<String, String> result = JsonMapFlattener.flatten(map);
|
||||
|
||||
assertThat(result).containsEntry("key.nested.anotherLevel", "value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFlattenNestedListOfSimpleObjects() throws Exception {
|
||||
|
||||
Map<String, Object> map = objectMapper.readValue(
|
||||
"{\"key\": [\"one\", \"two\"], \"dotted.key\": [\"one\", \"two\"] }",
|
||||
Map.class);
|
||||
Map<String, String> 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<String, Object> map = objectMapper.readValue(
|
||||
"{\"key\": [{ \"nested\":\"value\"}, { \"nested\":\"other-value\"}] }",
|
||||
Map.class);
|
||||
Map<String, String> result = JsonMapFlattener.flatten(map);
|
||||
|
||||
assertThat(result).containsEntry("key[0].nested", "value").containsEntry(
|
||||
"key[1].nested", "other-value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFlattenDeeplyNestedListOfComplexObject() throws Exception {
|
||||
|
||||
Map<String, Object> map = objectMapper
|
||||
.readValue(
|
||||
"{\"key\": { \"level1\": [{ \"nested\":\"value\"}, { \"nested\":\"other-value\"}]} }",
|
||||
Map.class);
|
||||
Map<String, String> result = JsonMapFlattener.flatten(map);
|
||||
|
||||
assertThat(result).containsEntry("key.level1[0].nested", "value").containsEntry(
|
||||
"key.level1[1].nested", "other-value");
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user