From c864bbaaed8c152ab98fc1492abd75353634d891 Mon Sep 17 00:00:00 2001 From: Ben Hale Date: Mon, 11 May 2020 15:33:47 -0700 Subject: [PATCH] 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 --- .../CassandraBindingsPropertiesProcessor.java | 19 ++-- .../boot/Db2BindingsPropertiesProcessor.java | 11 ++- .../cloud/bindings/boot/MapMapper.java | 82 ++++++++++++++++ .../MongoDbBindingsPropertiesProcessor.java | 4 +- .../MySqlBindingsPropertiesProcessor.java | 13 ++- .../OracleBindingsPropertiesProcessor.java | 11 ++- ...PostgreSqlBindingsPropertiesProcessor.java | 11 ++- ...{PutIfPresent.java => PropertyMapper.java} | 22 +---- .../RedisBindingsPropertiesProcessor.java | 8 +- .../SqlServerBindingsPropertiesProcessor.java | 11 ++- .../cloud/bindings/boot/MapMapperTest.java | 97 +++++++++++++++++++ .../cloud/bindings/boot/PutIfPresentTest.java | 54 ----------- 12 files changed, 231 insertions(+), 112 deletions(-) create mode 100644 src/main/java/org/springframework/cloud/bindings/boot/MapMapper.java rename src/main/java/org/springframework/cloud/bindings/boot/{PutIfPresent.java => PropertyMapper.java} (62%) create mode 100644 src/test/java/org/springframework/cloud/bindings/boot/MapMapperTest.java delete mode 100644 src/test/java/org/springframework/cloud/bindings/boot/PutIfPresentTest.java diff --git a/src/main/java/org/springframework/cloud/bindings/boot/CassandraBindingsPropertiesProcessor.java b/src/main/java/org/springframework/cloud/bindings/boot/CassandraBindingsPropertiesProcessor.java index b9bcaef..db4bc90 100644 --- a/src/main/java/org/springframework/cloud/bindings/boot/CassandraBindingsPropertiesProcessor.java +++ b/src/main/java/org/springframework/cloud/bindings/boot/CassandraBindingsPropertiesProcessor.java @@ -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 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"); }); } diff --git a/src/main/java/org/springframework/cloud/bindings/boot/Db2BindingsPropertiesProcessor.java b/src/main/java/org/springframework/cloud/bindings/boot/Db2BindingsPropertiesProcessor.java index deafc21..9d41147 100644 --- a/src/main/java/org/springframework/cloud/bindings/boot/Db2BindingsPropertiesProcessor.java +++ b/src/main/java/org/springframework/cloud/bindings/boot/Db2BindingsPropertiesProcessor.java @@ -43,13 +43,14 @@ public final class Db2BindingsPropertiesProcessor implements BindingsPropertiesP } bindings.filterBindings(KIND).forEach(binding -> { - Map 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")); }); } diff --git a/src/main/java/org/springframework/cloud/bindings/boot/MapMapper.java b/src/main/java/org/springframework/cloud/bindings/boot/MapMapper.java new file mode 100644 index 0000000..d65fb77 --- /dev/null +++ b/src/main/java/org/springframework/cloud/bindings/boot/MapMapper.java @@ -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 source; + + private final Map destination; + + MapMapper(Map source, Map destination) { + this.source = source; + this.destination = destination; + } + + Source from(String... keys) { + return new Source(keys); + } + + interface TriFunction { + 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 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 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]))); + } + + } + +} diff --git a/src/main/java/org/springframework/cloud/bindings/boot/MongoDbBindingsPropertiesProcessor.java b/src/main/java/org/springframework/cloud/bindings/boot/MongoDbBindingsPropertiesProcessor.java index f3dd6e7..9093e66 100644 --- a/src/main/java/org/springframework/cloud/bindings/boot/MongoDbBindingsPropertiesProcessor.java +++ b/src/main/java/org/springframework/cloud/bindings/boot/MongoDbBindingsPropertiesProcessor.java @@ -41,9 +41,9 @@ public final class MongoDbBindingsPropertiesProcessor implements BindingsPropert } bindings.filterBindings(KIND).forEach(binding -> { - Map 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"); }); } diff --git a/src/main/java/org/springframework/cloud/bindings/boot/MySqlBindingsPropertiesProcessor.java b/src/main/java/org/springframework/cloud/bindings/boot/MySqlBindingsPropertiesProcessor.java index 9904d4b..00c511f 100644 --- a/src/main/java/org/springframework/cloud/bindings/boot/MySqlBindingsPropertiesProcessor.java +++ b/src/main/java/org/springframework/cloud/bindings/boot/MySqlBindingsPropertiesProcessor.java @@ -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 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")); }); } diff --git a/src/main/java/org/springframework/cloud/bindings/boot/OracleBindingsPropertiesProcessor.java b/src/main/java/org/springframework/cloud/bindings/boot/OracleBindingsPropertiesProcessor.java index 13395aa..694727d 100644 --- a/src/main/java/org/springframework/cloud/bindings/boot/OracleBindingsPropertiesProcessor.java +++ b/src/main/java/org/springframework/cloud/bindings/boot/OracleBindingsPropertiesProcessor.java @@ -43,13 +43,14 @@ public final class OracleBindingsPropertiesProcessor implements BindingsProperti } bindings.filterBindings(KIND).forEach(binding -> { - Map 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")); }); } diff --git a/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessor.java b/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessor.java index b5fbdfd..8f57bfc 100644 --- a/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessor.java +++ b/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessor.java @@ -43,13 +43,14 @@ public final class PostgreSqlBindingsPropertiesProcessor implements BindingsProp } bindings.filterBindings(KIND).forEach(binding -> { - Map 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")); }); } diff --git a/src/main/java/org/springframework/cloud/bindings/boot/PutIfPresent.java b/src/main/java/org/springframework/cloud/bindings/boot/PropertyMapper.java similarity index 62% rename from src/main/java/org/springframework/cloud/bindings/boot/PutIfPresent.java rename to src/main/java/org/springframework/cloud/bindings/boot/PropertyMapper.java index df24889..d477389 100644 --- a/src/main/java/org/springframework/cloud/bindings/boot/PutIfPresent.java +++ b/src/main/java/org/springframework/cloud/bindings/boot/PropertyMapper.java @@ -18,27 +18,15 @@ package org.springframework.cloud.bindings.boot; import java.util.Map; -final class PutIfPresent { +final class PropertyMapper { + + private final Map source; private final Map destination; - private final String key; - - PutIfPresent(Map destination, String key) { + PropertyMapper(Map source, Map destination) { + this.source = source; this.destination = destination; - this.key = key; - } - - static PutIfPresent put(Map destination, String key) { - return new PutIfPresent(destination, key); - } - - void ifPresent(Map source, String key) { - if (!source.containsKey(key)) { - return; - } - - destination.put(this.key, source.get(key)); } } diff --git a/src/main/java/org/springframework/cloud/bindings/boot/RedisBindingsPropertiesProcessor.java b/src/main/java/org/springframework/cloud/bindings/boot/RedisBindingsPropertiesProcessor.java index 7fe94ed..faa7384 100644 --- a/src/main/java/org/springframework/cloud/bindings/boot/RedisBindingsPropertiesProcessor.java +++ b/src/main/java/org/springframework/cloud/bindings/boot/RedisBindingsPropertiesProcessor.java @@ -41,11 +41,11 @@ public final class RedisBindingsPropertiesProcessor implements BindingsPropertie } bindings.filterBindings(KIND).forEach(binding -> { - Map 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"); }); } diff --git a/src/main/java/org/springframework/cloud/bindings/boot/SqlServerBindingsPropertiesProcessor.java b/src/main/java/org/springframework/cloud/bindings/boot/SqlServerBindingsPropertiesProcessor.java index 43ef069..fc667f8 100644 --- a/src/main/java/org/springframework/cloud/bindings/boot/SqlServerBindingsPropertiesProcessor.java +++ b/src/main/java/org/springframework/cloud/bindings/boot/SqlServerBindingsPropertiesProcessor.java @@ -43,13 +43,14 @@ public final class SqlServerBindingsPropertiesProcessor implements BindingsPrope } bindings.filterBindings(KIND).forEach(binding -> { - Map 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")); }); } diff --git a/src/test/java/org/springframework/cloud/bindings/boot/MapMapperTest.java b/src/test/java/org/springframework/cloud/bindings/boot/MapMapperTest.java new file mode 100644 index 0000000..b18ac18 --- /dev/null +++ b/src/test/java/org/springframework/cloud/bindings/boot/MapMapperTest.java @@ -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 destination = new HashMap<>(); + + private final Map 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"); + } + +} diff --git a/src/test/java/org/springframework/cloud/bindings/boot/PutIfPresentTest.java b/src/test/java/org/springframework/cloud/bindings/boot/PutIfPresentTest.java deleted file mode 100644 index 88cdecb..0000000 --- a/src/test/java/org/springframework/cloud/bindings/boot/PutIfPresentTest.java +++ /dev/null @@ -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 source = Collections.singletonMap("test-source-key", "test-source-value"); - Map 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 source = Collections.emptyMap(); - Map destination = new HashMap<>(); - - put(destination, "test-destination-key").ifPresent(source, "test-source-key"); - - assertThat(destination).doesNotContainKey("test-destination-key"); - } - -}