From e2609d066cf8271641e2f452775a58f75af11b59 Mon Sep 17 00:00:00 2001 From: John Blum Date: Thu, 10 Dec 2020 11:07:41 -0800 Subject: [PATCH] Add prototype implementation of the Spring Cloud Bindings BingindsPropertyProcessor interface. Resolves gh-100. --- .../geode/cloud/bindings/Guards.java | 46 +++++ .../geode/cloud/bindings/MapMapper.java | 118 +++++++++++++ .../GeodeBindingsPropertiesProcessor.java | 73 ++++++++ .../main/resources/META-INF/spring.factories | 2 + ...eBindingsPropertiesProcessorUnitTests.java | 162 ++++++++++++++++++ 5 files changed, 401 insertions(+) create mode 100644 spring-geode-cloud/src/main/java/org/springframework/geode/cloud/bindings/Guards.java create mode 100644 spring-geode-cloud/src/main/java/org/springframework/geode/cloud/bindings/MapMapper.java create mode 100644 spring-geode-cloud/src/main/java/org/springframework/geode/cloud/bindings/boot/GeodeBindingsPropertiesProcessor.java create mode 100644 spring-geode-cloud/src/main/resources/META-INF/spring.factories create mode 100644 spring-geode-cloud/src/test/java/org/springframework/geode/cloud/bindings/boot/GeodeBindingsPropertiesProcessorUnitTests.java diff --git a/spring-geode-cloud/src/main/java/org/springframework/geode/cloud/bindings/Guards.java b/spring-geode-cloud/src/main/java/org/springframework/geode/cloud/bindings/Guards.java new file mode 100644 index 00000000..8456887c --- /dev/null +++ b/spring-geode-cloud/src/main/java/org/springframework/geode/cloud/bindings/Guards.java @@ -0,0 +1,46 @@ +/* + * 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 + * + * https://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.geode.cloud.bindings; + +import org.springframework.core.env.Environment; + +/** + * Abstract utility class to assess the Spring {@link Environment} + * + * @author John Blum + * @see org.springframework.core.env.Environment + * @since 1.4.1 + */ +@SuppressWarnings("unused") +public abstract class Guards { + + public static final String SPRING_CLOUD_BOOT_BINDINGS_ENABLED_PROPERTY = + "org.springframework.cloud.bindings.boot.enable"; + + public static final String SPRING_CLOUD_BOOT_BINDINGS_TYPE_ENABLED_PROPERTY = + "org.springframework.cloud.bindings.boot.%s.enable"; + + public static boolean isGlobalEnabled(Environment environment) { + return environment.getProperty(SPRING_CLOUD_BOOT_BINDINGS_ENABLED_PROPERTY, Boolean.class, false); + } + + public static boolean isTypeEnabled(Environment environment, String type) { + + String propertyName = String.format(SPRING_CLOUD_BOOT_BINDINGS_TYPE_ENABLED_PROPERTY, type); + + return environment.getProperty(propertyName, Boolean.class, true); + } +} diff --git a/spring-geode-cloud/src/main/java/org/springframework/geode/cloud/bindings/MapMapper.java b/spring-geode-cloud/src/main/java/org/springframework/geode/cloud/bindings/MapMapper.java new file mode 100644 index 00000000..577f107a --- /dev/null +++ b/spring-geode-cloud/src/main/java/org/springframework/geode/cloud/bindings/MapMapper.java @@ -0,0 +1,118 @@ +/* + * 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 + * + * https://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.geode.cloud.bindings; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Maps a source set of properties to a target set of properties. + * + * @author John Blum + * @see java.util.Map + * @see java.util.function.Function + * @since 1.4.1 + */ +@SuppressWarnings("unused") +public class MapMapper { + + private final Map source; + private final Map target; + + public MapMapper(@NonNull Map source, @NonNull Map target) { + + Assert.notNull(source, "Source Map must not be null"); + Assert.notNull(target, "Target Map must not be null"); + + this.source = source; + this.target = target; + } + + protected @NonNull Map getSource() { + return source; + } + + protected @NonNull Map getTarget() { + return target; + } + + @SuppressWarnings("all") + public Source from(@NonNull String... keys) { + + String[] resolvedKeys = Arrays.stream(ArrayUtils.nullSafeArray(keys, String.class)) + .filter(StringUtils::hasText) + .collect(Collectors.toList()) + .toArray(new String[0]); + + return new Source(resolvedKeys); + } + + protected interface TriFunction { + R apply(T t, U u, V v); + } + + public class Source { + + private final String[] keys; + + private Source(@NonNull String[] keys) { + + Assert.notNull(keys, "The String array of keys must not be null"); + + this.keys = keys; + } + + public void to(String key) { + to(key, v -> v); + } + + public void to(@NonNull String key, @NonNull Function function) { + + String[] keys = this.keys; + + Assert.state(keys.length == 1, + String.format("Source size [%d] cannot be transformed as one argument", keys.length)); + + Map source = getSource(); + + if (Arrays.stream(keys).allMatch(source::containsKey)) { + getTarget().put(key, function.apply(source.get(keys[0]))); + } + } + + public void to(String key, TriFunction function) { + + String[] keys = this.keys; + + Assert.state(keys.length == 3, + String.format("Source size [%d] cannot be consumed as three arguments", keys.length)); + + Map source = getSource(); + + if (Arrays.stream(keys).allMatch(source::containsKey)) { + getTarget().put(key, function.apply(source.get(keys[0]), source.get(keys[1]), source.get(keys[2]))); + } + } + + } +} diff --git a/spring-geode-cloud/src/main/java/org/springframework/geode/cloud/bindings/boot/GeodeBindingsPropertiesProcessor.java b/spring-geode-cloud/src/main/java/org/springframework/geode/cloud/bindings/boot/GeodeBindingsPropertiesProcessor.java new file mode 100644 index 00000000..89127828 --- /dev/null +++ b/spring-geode-cloud/src/main/java/org/springframework/geode/cloud/bindings/boot/GeodeBindingsPropertiesProcessor.java @@ -0,0 +1,73 @@ +/* + * 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 + * + * https://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.geode.cloud.bindings.boot; + +import java.util.Map; + +import org.springframework.cloud.bindings.Bindings; +import org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor; +import org.springframework.core.env.Environment; +import org.springframework.geode.cloud.bindings.Guards; +import org.springframework.geode.cloud.bindings.MapMapper; +import org.springframework.lang.NonNull; + +/** + * A Spring Cloud Bindings {@link BindingsPropertiesProcessor} for Apache Geode. + * + * @author John Blum + * @see java.util.Map + * @see org.springframework.cloud.bindings.Bindings + * @see org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor + * @see org.springframework.core.env.Environment + * @see org.springframework.geode.cloud.bindings.Guards + * @see org.springframework.geode.cloud.bindings.MapMapper + * @see Guards + * @since 1.4.1 + */ +@SuppressWarnings("unused") +public class GeodeBindingsPropertiesProcessor implements BindingsPropertiesProcessor { + + public static final String TYPE = "gemfire"; + + /** + * @inheritDoc + */ + @Override + public void process(@NonNull Environment environment, @NonNull Bindings bindings, + @NonNull Map properties) { + + if (Guards.isTypeEnabled(environment, TYPE)) { + + bindings.filterBindings(TYPE).forEach(binding -> { + + // TODO - Change! Current mappings and property configuration is based on VMware Tanzu GemFire for VMS + // (i.e. PCC in PCF). + + MapMapper mapMapper = new MapMapper(binding.getSecret(), properties); + + mapMapper.from("gemfire.security-username").to("spring.data.gemfire.security.username"); + mapMapper.from("gemfire.security-password").to("spring.data.gemfire.security.password"); + mapMapper.from("gemfire.locators").to("spring.data.gemfire.pool.locators"); + mapMapper.from("gemfire.http-service-bind-address").to("spring.data.gemfire.management.http.host"); + mapMapper.from("gemfire.http-service-port").to("spring.data.gemfire.management.http.port"); + + properties.put("spring.data.gemfire.management.require-https", true); + properties.put("spring.data.gemfire.management.use-http", true); + properties.put("spring.data.gemfire.security.ssl.use-default-context", true); + }); + } + } +} diff --git a/spring-geode-cloud/src/main/resources/META-INF/spring.factories b/spring-geode-cloud/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..6adb8da3 --- /dev/null +++ b/spring-geode-cloud/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor=\ + org.springframework.geode.cloud.bindings.boot.GeodeBindingsPropertiesProcessor diff --git a/spring-geode-cloud/src/test/java/org/springframework/geode/cloud/bindings/boot/GeodeBindingsPropertiesProcessorUnitTests.java b/spring-geode-cloud/src/test/java/org/springframework/geode/cloud/bindings/boot/GeodeBindingsPropertiesProcessorUnitTests.java new file mode 100644 index 00000000..f02a930c --- /dev/null +++ b/spring-geode-cloud/src/test/java/org/springframework/geode/cloud/bindings/boot/GeodeBindingsPropertiesProcessorUnitTests.java @@ -0,0 +1,162 @@ +/* + * 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 + * + * https://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.geode.cloud.bindings.boot; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.core.env.Environment; +import org.springframework.data.gemfire.tests.support.MapBuilder; +import org.springframework.geode.cloud.bindings.Guards; + +/** + * Unit Tests for {@link GeodeBindingsPropertiesProcessor}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.geode.cloud.bindings.boot.GeodeBindingsPropertiesProcessor + * @since 1.4.1 + */ +public class GeodeBindingsPropertiesProcessorUnitTests { + + @Test + public void processMapsCloudPropertiesToFrameworkPropertiesCorrectly() { + + String propertyName = String.format(Guards.SPRING_CLOUD_BOOT_BINDINGS_TYPE_ENABLED_PROPERTY, + GeodeBindingsPropertiesProcessor.TYPE); + + Environment mockEnvironment = mock(Environment.class); + + doReturn(true) + .when(mockEnvironment).getProperty(eq(propertyName), eq(Boolean.class), eq(true)); + + Path mockPath= mock(Path.class); + + Map secret = MapBuilder.newMapBuilder() + .put(Binding.TYPE, GeodeBindingsPropertiesProcessor.TYPE) + .put("gemfire.security-username", "DarthVader") + .put("gemfire.security-password", "s5!thL0rd") + .put("gemfire.locators", "mustafar[61616]") + .put("gemfire.http-service-bind-address", "10.100.101.69") + .put("gemfire.http-service-port", "7070") + .build(); + + Binding testBinding = new Binding("gemfire-test-binding-name", mockPath, secret); + + Bindings testBindings = new Bindings(testBinding); + + GeodeBindingsPropertiesProcessor bindingsPropertiesProcessor = new GeodeBindingsPropertiesProcessor(); + + Map properties = new HashMap<>(); + + bindingsPropertiesProcessor.process(mockEnvironment, testBindings, properties); + + assertThat(properties) + .containsEntry("spring.data.gemfire.security.username", secret.get("gemfire.security-username")) + .containsEntry("spring.data.gemfire.security.password", secret.get("gemfire.security-password")) + .containsEntry("spring.data.gemfire.pool.locators", secret.get("gemfire.locators")) + .containsEntry("spring.data.gemfire.management.http.host", secret.get("gemfire.http-service-bind-address")) + .containsEntry("spring.data.gemfire.management.http.port", secret.get("gemfire.http-service-port")); + + assertThat(Boolean.TRUE.equals(properties.get("spring.data.gemfire.management.require-https"))).isTrue(); + assertThat(Boolean.TRUE.equals(properties.get("spring.data.gemfire.management.use-http"))).isTrue(); + assertThat(Boolean.TRUE.equals(properties.get("spring.data.gemfire.security.ssl.use-default-context"))).isTrue(); + + verify(mockEnvironment, times(1)) + .getProperty(eq(propertyName), eq(Boolean.class), eq(true)); + + verifyNoMoreInteractions(mockEnvironment); + } + + @Test + public void processDoesNotMapPropertiesWhenGemFireTypeIsDisabled() { + + String propertyName = String.format(Guards.SPRING_CLOUD_BOOT_BINDINGS_TYPE_ENABLED_PROPERTY, + GeodeBindingsPropertiesProcessor.TYPE); + + Environment mockEnvironment = mock(Environment.class); + + doReturn(false) + .when(mockEnvironment).getProperty(eq(propertyName), eq(Boolean.class), eq(true)); + + Path mockPath= mock(Path.class); + + Binding testBinding = new Binding("gemfire-test-binding-name", mockPath, + Collections.singletonMap(Binding.TYPE, GeodeBindingsPropertiesProcessor.TYPE)); + + Bindings testBindings = new Bindings(testBinding); + + GeodeBindingsPropertiesProcessor bindingsPropertiesProcessor = new GeodeBindingsPropertiesProcessor(); + + Map properties = new HashMap<>(); + + bindingsPropertiesProcessor.process(mockEnvironment, testBindings, properties); + + assertThat(properties).isEmpty(); + + verify(mockEnvironment, times(1)) + .getProperty(eq(propertyName), eq(Boolean.class), eq(true)); + + verifyNoMoreInteractions(mockEnvironment); + } + + @Test + public void processDoesNotMapPropertiesWhenGemFireTypeIsNotPresent() { + + String propertyName = String.format(Guards.SPRING_CLOUD_BOOT_BINDINGS_TYPE_ENABLED_PROPERTY, + GeodeBindingsPropertiesProcessor.TYPE); + + Environment mockEnvironment = mock(Environment.class); + + doReturn(true) + .when(mockEnvironment).getProperty(eq(propertyName), eq(Boolean.class), eq(true)); + + Path mockPath= mock(Path.class); + + Binding testBinding = new Binding("mock-test-binding-name", mockPath, + Collections.singletonMap(Binding.TYPE, "mock")); + + Bindings testBindings = new Bindings(testBinding); + + GeodeBindingsPropertiesProcessor bindingsPropertiesProcessor = new GeodeBindingsPropertiesProcessor(); + + Map properties = new HashMap<>(); + + bindingsPropertiesProcessor.process(mockEnvironment, testBindings, properties); + + assertThat(properties).isEmpty(); + + verify(mockEnvironment, times(1)) + .getProperty(eq(propertyName), eq(Boolean.class), eq(true)); + + verifyNoMoreInteractions(mockEnvironment); + } +}