Improve Property Mapping API

Previously the PutIfPresent API was a clunky way of only setting a property in
the PropertySource if the corresponding entry from the binding existed.  It
was pointed out in #31 that there are better APIs for doing this same thing.
Unfortunately the suggestion in that issue wasn't suitable (it was built for
beans not maps), but it did inspire a better API that was suited to maps.
This change implements that new API.

Signed-off-by: Ben Hale <bhale@vmware.com>
This commit is contained in:
Ben Hale
2020-05-11 15:33:47 -07:00
parent e3384e82f6
commit c864bbaaed
12 changed files with 231 additions and 112 deletions

View File

@@ -23,7 +23,6 @@ import org.springframework.core.env.Environment;
import java.util.Map;
import static org.springframework.cloud.bindings.boot.Guards.isKindEnabled;
import static org.springframework.cloud.bindings.boot.PutIfPresent.put;
/**
* An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s of kind: {@value KIND}.
@@ -42,16 +41,16 @@ public final class CassandraBindingsPropertiesProcessor implements BindingsPrope
}
bindings.filterBindings(KIND).forEach(binding -> {
Map<String, String> secret = binding.getSecret();
MapMapper map = new MapMapper(binding.getSecret(), properties);
put(properties, "spring.data.cassandra.cluster-name").ifPresent(secret, "cluster-name");
put(properties, "spring.data.cassandra.compression").ifPresent(secret, "compression");
put(properties, "spring.data.cassandra.contact-points").ifPresent(secret, "contact-points");
put(properties, "spring.data.cassandra.keyspace-name").ifPresent(secret, "keyspace-name");
put(properties, "spring.data.cassandra.password").ifPresent(secret, "password");
put(properties, "spring.data.cassandra.port").ifPresent(secret, "port");
put(properties, "spring.data.cassandra.ssl").ifPresent(secret, "ssl");
put(properties, "spring.data.cassandra.username").ifPresent(secret, "username");
map.from("cluster-name").to("spring.data.cassandra.cluster-name");
map.from("compression").to("spring.data.cassandra.compression");
map.from("contact-points").to("spring.data.cassandra.contact-points");
map.from("keyspace-name").to("spring.data.cassandra.keyspace-name");
map.from("password").to("spring.data.cassandra.password");
map.from("port").to("spring.data.cassandra.port");
map.from("ssl").to("spring.data.cassandra.ssl");
map.from("username").to("spring.data.cassandra.username");
});
}

View File

@@ -43,13 +43,14 @@ public final class Db2BindingsPropertiesProcessor implements BindingsPropertiesP
}
bindings.filterBindings(KIND).forEach(binding -> {
Map<String, String> secret = binding.getSecret();
MapMapper map = new MapMapper(binding.getSecret(), properties);
map.from("password").to("spring.datasource.password");
map.from("host", "port", "database").to("spring.datasource.url",
(host, port, database) -> String.format("jdbc:db2://%s:%s/%s", host, port, database));
map.from("username").to("spring.datasource.username");
properties.put("spring.datasource.driver-class-name", "com.ibm.db2.jcc.DB2Driver");
properties.put("spring.datasource.password", secret.get("password"));
properties.put("spring.datasource.url", String.format("jdbc:db2://%s:%s/%s",
secret.get("host"), secret.get("port"), secret.get("database")));
properties.put("spring.datasource.username", secret.get("username"));
});
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2020 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.cloud.bindings.boot;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
final class MapMapper {
private final Map<String, String> source;
private final Map<String, Object> destination;
MapMapper(Map<String, String> source, Map<String, Object> destination) {
this.source = source;
this.destination = destination;
}
Source from(String... keys) {
return new Source(keys);
}
interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
final class Source {
private final String[] keys;
private Source(String[] keys) {
this.keys = keys;
}
void to(String key) {
to(key, v -> v);
}
void to(String key, Function<String, Object> function) {
if (keys.length != 1) {
throw new IllegalStateException(
String.format("source size %d cannot be transformed as one argument", keys.length));
}
if (!Arrays.stream(keys).allMatch(source::containsKey)) {
return;
}
destination.put(key, function.apply(source.get(keys[0])));
}
void to(String key, TriFunction<String, String, String, Object> function) {
if (keys.length != 3) {
throw new IllegalStateException(
String.format("source size %d cannot be consumed as three arguments", keys.length));
}
if (!Arrays.stream(keys).allMatch(source::containsKey)) {
return;
}
destination.put(key, function.apply(source.get(keys[0]), source.get(keys[1]), source.get(keys[2])));
}
}
}

View File

@@ -41,9 +41,9 @@ public final class MongoDbBindingsPropertiesProcessor implements BindingsPropert
}
bindings.filterBindings(KIND).forEach(binding -> {
Map<String, String> secret = binding.getSecret();
MapMapper map = new MapMapper(binding.getSecret(), properties);
properties.put("spring.mongodb.uri", secret.get("uri"));
map.from("uri").to("spring.mongodb.uri");
});
}

View File

@@ -43,6 +43,14 @@ public final class MySqlBindingsPropertiesProcessor implements BindingsPropertie
}
bindings.filterBindings(KIND).forEach(binding -> {
MapMapper map = new MapMapper(binding.getSecret(), properties);
map.from("password").to("spring.datasource.password");
map.from("host", "port", "database").to("spring.datasource.url",
(host, port, database) -> String.format("jdbc:mysql://%s:%s/%s", host, port, database));
map.from("username").to("spring.datasource.username");
Map<String, String> secret = binding.getSecret();
try {
@@ -55,11 +63,6 @@ public final class MySqlBindingsPropertiesProcessor implements BindingsPropertie
} catch (ClassNotFoundException ignored) {
}
}
properties.put("spring.datasource.password", secret.get("password"));
properties.put("spring.datasource.url", String.format("jdbc:mysql://%s:%s/%s",
secret.get("host"), secret.get("port"), secret.get("database")));
properties.put("spring.datasource.username", secret.get("username"));
});
}

View File

@@ -43,13 +43,14 @@ public final class OracleBindingsPropertiesProcessor implements BindingsProperti
}
bindings.filterBindings(KIND).forEach(binding -> {
Map<String, String> secret = binding.getSecret();
MapMapper map = new MapMapper(binding.getSecret(), properties);
map.from("password").to("spring.datasource.password");
map.from("host", "port", "database").to("spring.datasource.url",
(host, port, database) -> String.format("jdbc:oracle://%s:%s/%s", host, port, database));
map.from("username").to("spring.datasource.username");
properties.put("spring.datasource.driver-class-name", "oracle.jdbc.OracleDriver");
properties.put("spring.datasource.password", secret.get("password"));
properties.put("spring.datasource.url", String.format("jdbc:oracle://%s:%s/%s",
secret.get("host"), secret.get("port"), secret.get("database")));
properties.put("spring.datasource.username", secret.get("username"));
});
}

View File

@@ -43,13 +43,14 @@ public final class PostgreSqlBindingsPropertiesProcessor implements BindingsProp
}
bindings.filterBindings(KIND).forEach(binding -> {
Map<String, String> secret = binding.getSecret();
MapMapper map = new MapMapper(binding.getSecret(), properties);
map.from("password").to("spring.datasource.password");
map.from("host", "port", "database").to("spring.datasource.url",
(host, port, database) -> String.format("jdbc:postgres://%s:%s/%s", host, port, database));
map.from("username").to("spring.datasource.username");
properties.put("spring.datasource.driver-class-name", "org.postgresql.Driver");
properties.put("spring.datasource.password", secret.get("password"));
properties.put("spring.datasource.url", String.format("jdbc:postgres://%s:%s/%s",
secret.get("host"), secret.get("port"), secret.get("database")));
properties.put("spring.datasource.username", secret.get("username"));
});
}

View File

@@ -18,27 +18,15 @@ package org.springframework.cloud.bindings.boot;
import java.util.Map;
final class PutIfPresent {
final class PropertyMapper {
private final Map<String, String> source;
private final Map<String, Object> destination;
private final String key;
PutIfPresent(Map<String, Object> destination, String key) {
PropertyMapper(Map<String, String> source, Map<String, Object> destination) {
this.source = source;
this.destination = destination;
this.key = key;
}
static PutIfPresent put(Map<String, Object> destination, String key) {
return new PutIfPresent(destination, key);
}
void ifPresent(Map<String, String> source, String key) {
if (!source.containsKey(key)) {
return;
}
destination.put(this.key, source.get(key));
}
}

View File

@@ -41,11 +41,11 @@ public final class RedisBindingsPropertiesProcessor implements BindingsPropertie
}
bindings.filterBindings(KIND).forEach(binding -> {
Map<String, String> secret = binding.getSecret();
MapMapper map = new MapMapper(binding.getSecret(), properties);
properties.put("spring.redis.host", secret.get("host"));
properties.put("spring.redis.password", secret.get("password"));
properties.put("spring.redis.port", secret.get("port"));
map.from("host").to("spring.redis.host");
map.from("password").to("spring.redis.password");
map.from("port").to("spring.redis.port");
});
}

View File

@@ -43,13 +43,14 @@ public final class SqlServerBindingsPropertiesProcessor implements BindingsPrope
}
bindings.filterBindings(KIND).forEach(binding -> {
Map<String, String> secret = binding.getSecret();
MapMapper map = new MapMapper(binding.getSecret(), properties);
map.from("password").to("spring.datasource.password");
map.from("host", "port", "database").to("spring.datasource.url",
(host, port, database) -> String.format("jdbc:sqlserver://%s:%s/%s", host, port, database));
map.from("username").to("spring.datasource.username");
properties.put("spring.datasource.driver-class-name", "com.microsoft.sqlserver.jdbc.SQLServerDriver");
properties.put("spring.datasource.password", secret.get("password"));
properties.put("spring.datasource.url", String.format("jdbc:sqlserver://%s:%s/%s",
secret.get("host"), secret.get("port"), secret.get("database")));
properties.put("spring.datasource.username", secret.get("username"));
});
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2020 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.cloud.bindings.boot;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Map Mapper test")
final class MapMapperTest {
private final Map<String, Object> destination = new HashMap<>();
private final Map<String, String> source = new HashMap<>();
private final MapMapper map = new MapMapper(source, destination);
@Test
@DisplayName("puts if present")
void present() {
source.put("test-source-key", "test-source-value");
map.from("test-source-key").to("test-destination-key");
assertThat(destination).containsEntry("test-destination-key", "test-source-value");
}
@Test
@DisplayName("transforms source value")
void transformed() {
source.put("test-source-key", "test-source-value");
map.from("test-source-key").to("test-destination-key", s -> {
assertThat(s).isEqualTo("test-source-value");
return "test-destination-value";
});
assertThat(destination).containsEntry("test-destination-key", "test-destination-value");
}
@Test
@DisplayName("does not put if not present")
void notPresent() {
map.from("test-source-key").to("test-destination-key");
assertThat(destination).doesNotContainKey("test-destination-key");
}
@Test
@DisplayName("puts if all present")
void allPresent() {
source.put("test-source-key-1", "test-source-value-1");
source.put("test-source-key-2", "test-source-value-2");
source.put("test-source-key-3", "test-source-value-3");
map.from("test-source-key-1", "test-source-key-2", "test-source-key-3").to("test-destination-key", (a, b, c) -> {
assertThat(a).isEqualTo("test-source-value-1");
assertThat(b).isEqualTo("test-source-value-2");
assertThat(c).isEqualTo("test-source-value-3");
return "test-destination-value";
});
assertThat(destination).containsEntry("test-destination-key", "test-destination-value");
}
@Test
@DisplayName("does not put if not all present")
void notAllPresent() {
source.put("test-source-key-1", "test-source-value-1");
source.put("test-source-key-2", "test-source-value-2");
map.from("test-source-key-1", "test-source-key-2", "test-source-key-3").to("test-destination-key", (a, b, c) -> "test-destination-value");
assertThat(destination).doesNotContainKey("test-destination-key");
}
}

View File

@@ -1,54 +0,0 @@
/*
* Copyright 2020 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.cloud.bindings.boot;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.bindings.boot.PutIfPresent.put;
@DisplayName("Put if present")
final class PutIfPresentTest {
@Test
@DisplayName("puts if present")
void present() {
Map<String, String> source = Collections.singletonMap("test-source-key", "test-source-value");
Map<String, Object> destination = new HashMap<>();
put(destination, "test-destination-key").ifPresent(source, "test-source-key");
assertThat(destination).containsEntry("test-destination-key", "test-source-value");
}
@Test
@DisplayName("does not put if not present")
void notPresent() {
Map<String, String> source = Collections.emptyMap();
Map<String, Object> destination = new HashMap<>();
put(destination, "test-destination-key").ifPresent(source, "test-source-key");
assertThat(destination).doesNotContainKey("test-destination-key");
}
}