From 0cb7c25754b31cfade104ded9c2ef00f74162eca Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Tue, 9 May 2017 09:47:19 -0500 Subject: [PATCH] Add service data postprocessor extension point to cf connector. --- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- .../cloudfoundry/CloudFoundryConnector.java | 24 +++-- .../CloudFoundryRawServiceData.java | 22 +++++ .../ServiceDataPostProcessor.java | 83 ++++++++++++++++++ .../CloudFoundryConnectorApplicationTest.java | 14 +++ .../StubServiceDataPostProcessor.java | 24 +++++ ...loud.cloudfoundry.ServiceDataPostProcessor | 1 + .../test-credentials-post-processed.json | 8 ++ .../springframework/cloud/CloudTestUtil.java | 6 +- 10 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 spring-cloud-cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/CloudFoundryRawServiceData.java create mode 100644 spring-cloud-cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/ServiceDataPostProcessor.java create mode 100644 spring-cloud-cloudfoundry-connector/src/test/java/org/springframework/cloud/cloudfoundry/StubServiceDataPostProcessor.java create mode 100644 spring-cloud-cloudfoundry-connector/src/test/resources/META-INF/services/org.springframework.cloud.cloudfoundry.ServiceDataPostProcessor create mode 100644 spring-cloud-cloudfoundry-connector/src/test/resources/org/springframework/cloud/cloudfoundry/test-credentials-post-processed.json diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 941144813d241db74e1bf25b6804c679fbe7f0a3..13372aef5e24af05341d49695ee84e5f9b594659 100644 GIT binary patch delta 1239 zcmZo!%-ph=nK!_jnT3mifrEoVYtz(;ylT8a%KiG(8A0|83=E+Y9fCz9nZAB1Vq`!l z6ay+;uE-3cPHTXvZw4T0vL@q05WSg~sh9~Y(9H52ERf5(1T3&wghQAOETYAC6C%PU zXut{yi zcUjN>JuPF&vh)zstp(jXSw5}N&DU7wQkEuonMJRjE$UOWu9)qmIg;~kmtXJ_YDML!#U zLG*uXy2=g~IoEZOQ4plfC?|ZI7ZU>m3s9@urNtTE2}1EY-b+8RC|S>pwuGS~kT(?0d4=5u!$8#|4PU@0}q`V2jtvpbqMPUKacAZLxh?ZvEyLuu`|X7>Q;gAM`vcm{TEI}tJz#>G zM<%a3UX2h$P<&A@cvK|3%#>7XB&eta+=Ootxw1=C9pMS$o@hvUH9#KU0j zh{Odr3HhKM`Ui~ay#mjB}b delta 1239 zcmZ9LYfO_@7{^chQmNFU2sj~{)(I-iX>DPd65F~OP%bS^Zz_UdOJN&KK`x1Acq7gm zVdZ#cC*@~N20ouzMaJ5=`egiapDHFYDaSDG+o+SI zm?U<)RN|1P7_q!iD%{Ov6&~yj2AHkJ<8KSaM>X5Nafz37PhLze{m5(<-Fa@jbvquzvuKelC$nkkT{{cdM-3hckthBr4aHiL?{g!H$LFu zMMJUl58W9QpIw`0tu6t-`Bf49;fv{#recNj6N>p8xZIiY|388|oT=fAd6feakncEbi6wgSmM?Ke{p947xt1oORo;} zyX0> { @@ -25,10 +25,13 @@ public class CloudFoundryConnector extends AbstractCloudConnector serviceDataPostProcessors; + @SuppressWarnings({ "unchecked", "rawtypes" }) public CloudFoundryConnector() { super((Class) CloudFoundryServiceInfoCreator.class); + scanServiceDataPostProcessors(); } @Override @@ -64,20 +67,25 @@ public class CloudFoundryConnector extends AbstractCloudConnector> getServicesData() { String servicesString = environment.getEnvValue("VCAP_SERVICES"); - Map>> rawServices = new HashMap>>(); + CloudFoundryRawServiceData rawServices = new CloudFoundryRawServiceData(); if (servicesString != null && servicesString.length() > 0) { try { - rawServices = objectMapper.readValue(servicesString, Map.class); + rawServices = objectMapper.readValue(servicesString, CloudFoundryRawServiceData.class); } catch (Exception e) { throw new CloudException(e); } } - List> flatServices = new ArrayList>(); + for (ServiceDataPostProcessor postProcessor : serviceDataPostProcessors) { + rawServices = postProcessor.process(rawServices); + } + + List> flatServices = new ArrayList>(); for (Map.Entry>> entry : rawServices.entrySet()) { flatServices.addAll(entry.getValue()); } + return flatServices; } @@ -86,6 +94,10 @@ public class CloudFoundryConnector extends AbstractCloudConnector> getFallbackServiceInfoCreator() { return new CloudFoundryFallbackServiceInfoCreator(); } + + private void scanServiceDataPostProcessors() { + serviceDataPostProcessors = ServiceLoader.load(ServiceDataPostProcessor.class); + } } class CloudFoundryFallbackServiceInfoCreator extends FallbackServiceInfoCreator> { diff --git a/spring-cloud-cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/CloudFoundryRawServiceData.java b/spring-cloud-cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/CloudFoundryRawServiceData.java new file mode 100644 index 0000000..1546d17 --- /dev/null +++ b/spring-cloud-cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/CloudFoundryRawServiceData.java @@ -0,0 +1,22 @@ +package org.springframework.cloud.cloudfoundry; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A {@link CloudFoundryRawServiceData} object represents the data read from the {@literal VCAP_SERVICES} + * environment variable and transformed from JSON text to a collection of objects. + * + * The root of the data structure is a {@link List}. Each element of the list represents one service from + * the JSON. + */ +public class CloudFoundryRawServiceData extends HashMap>> { + public CloudFoundryRawServiceData() { + super(); + } + + public CloudFoundryRawServiceData(Map>> map) { + super(map); + } +} diff --git a/spring-cloud-cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/ServiceDataPostProcessor.java b/spring-cloud-cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/ServiceDataPostProcessor.java new file mode 100644 index 0000000..8520289 --- /dev/null +++ b/spring-cloud-cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/ServiceDataPostProcessor.java @@ -0,0 +1,83 @@ +package org.springframework.cloud.cloudfoundry; + +/** + * An extension point that allows service data to be processed after it is read from {@literal VCAP_SERVICES}. + */ +public interface ServiceDataPostProcessor { + /** + * Process raw service data as read from {@literal VCAP_SERVICES}. + * + * This method will be called after the {@literal VCAP_SERVICES} environment variable has been read from + * the environment and transformed from JSON text into a {@link CloudFoundryRawServiceData} data structure. + * + * If the {@literal VCAP_SERVICES} environment variable for an application contains the following: + * + *
+	 * {@code
+	 * "VCAP_SERVICES": {
+	 *   "mysql": [
+	 *     {
+	 *       "label": "mysql",
+	 *       "name": "mysql-db",
+	 *       "plan": "100mb",
+	 *       "tags": [ "mysql", "relational" ],
+	 *       "credentials": {
+	 *         "jdbcUrl": "jdbc:mysql://mysql-broker:3306/db?user=username\u0026password=password",
+	 *         "uri": "mysql://username:password@mysql-broker:3306/db?reconnect=true",
+	 *       }
+	 *     }
+	 *   ],
+	 *   "rabbitmq": [
+	 *     {
+	 *       "label": "rabbitmq",
+	 *       "name": "rabbit-queue",
+	 *       "plan": "standard",
+	 *       "tags": [ "rabbitmq", "messaging" ],
+	 *       "credentials": {
+	 *         "http_api_uri": "http://username:password@rabbitmq-broker:12345/api",
+	 *         "uri": "amqp://username:password@rabbitmq-broker/vhost",
+	 *       }
+	 *     }
+	 *   ]
+	 * }
+	 * }
+	 * 
+ * + * Then the {@link CloudFoundryRawServiceData} data structure would contain the equivalent of this: + * + *
+	 * {@code
+	 * {
+	 *   "mysql": [
+	 *     {
+	 *       "label": "mysql",
+	 *       "name": "mysql-db",
+	 *       "plan": "100mb",
+	 *       "tags": [ "mysql", "relational" ],
+	 *       "credentials": {
+	 *         "jdbcUrl": "jdbc:mysql://mysql-broker:3306/db?user=username\u0026password=password",
+	 *         "uri": "mysql://username:password@mysql-broker:3306/db?reconnect=true",
+	 *       }
+	 *     }
+	 *   ]
+	 *   "rabbitmq": [
+	 *     {
+	 *       "label": "rabbitmq",
+	 *       "name": "rabbit-queue",
+	 *       "plan": "standard",
+	 *       "tags": [ "rabbitmq", "messaging" ],
+	 *       "credentials": {
+	 *         "http_api_uri": "http://username:password@rabbitmq-broker:12345/api",
+	 *         "uri": "amqp://username:password@rabbitmq-broker/vhost",
+	 *       }
+	 *     }
+	 *   ]
+	 * }
+	 * }
+	 * 
+ * + * @param serviceData the service data parsed from {@literal VCAP_SERVICES} + * @return the provided {@literal serviceData} with modifications + */ + CloudFoundryRawServiceData process(CloudFoundryRawServiceData serviceData); +} diff --git a/spring-cloud-cloudfoundry-connector/src/test/java/org/springframework/cloud/cloudfoundry/CloudFoundryConnectorApplicationTest.java b/spring-cloud-cloudfoundry-connector/src/test/java/org/springframework/cloud/cloudfoundry/CloudFoundryConnectorApplicationTest.java index 54ed630..880af4a 100644 --- a/spring-cloud-cloudfoundry-connector/src/test/java/org/springframework/cloud/cloudfoundry/CloudFoundryConnectorApplicationTest.java +++ b/spring-cloud-cloudfoundry-connector/src/test/java/org/springframework/cloud/cloudfoundry/CloudFoundryConnectorApplicationTest.java @@ -1,8 +1,10 @@ package org.springframework.cloud.cloudfoundry; +import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; @@ -12,6 +14,7 @@ import java.util.List; import org.junit.Test; import org.springframework.cloud.CloudException; import org.springframework.cloud.service.ServiceInfo; +import org.springframework.cloud.service.common.MysqlServiceInfo; /** * @@ -95,4 +98,15 @@ public class CloudFoundryConnectorApplicationTest extends AbstractCloudFoundryCo assertEquals(1, serviceInfos.size()); } + @Test + public void serviceInfosWithPostProcessedCredentials() { + when(mockEnvironment.getEnvValue("VCAP_SERVICES")). + thenReturn(getServicesPayload(readTestDataFile("test-credentials-post-processed.json"))); + List serviceInfos = testCloudConnector.getServiceInfos(); + assertNotNull(serviceInfos); + assertEquals(1, serviceInfos.size()); + assertThat(serviceInfos.get(0), instanceOf(MysqlServiceInfo.class)); + assertEquals(((MysqlServiceInfo) serviceInfos.get(0)).getUri(), "MYSQL://USERNAME:PASSWORD@DB.EXAMPLE.COM/DB"); + } + } diff --git a/spring-cloud-cloudfoundry-connector/src/test/java/org/springframework/cloud/cloudfoundry/StubServiceDataPostProcessor.java b/spring-cloud-cloudfoundry-connector/src/test/java/org/springframework/cloud/cloudfoundry/StubServiceDataPostProcessor.java new file mode 100644 index 0000000..dd13cd3 --- /dev/null +++ b/spring-cloud-cloudfoundry-connector/src/test/java/org/springframework/cloud/cloudfoundry/StubServiceDataPostProcessor.java @@ -0,0 +1,24 @@ +package org.springframework.cloud.cloudfoundry; + +import java.util.List; +import java.util.Map; + +public class StubServiceDataPostProcessor implements ServiceDataPostProcessor { + @SuppressWarnings("unchecked") + @Override + public CloudFoundryRawServiceData process(CloudFoundryRawServiceData serviceData) { + for (List> service : serviceData.values()) { + Map serviceMap = service.get(0); + String name = (String) serviceMap.get("name"); + if (name.equals("uppercase")) { + Map credentials = (Map) serviceMap.get("credentials"); + for (Map.Entry entry : credentials.entrySet()) { + String upperCaseValue = ((String) entry.getValue()).toUpperCase(); + credentials.put(entry.getKey(), upperCaseValue); + } + } + } + + return serviceData; + } +} diff --git a/spring-cloud-cloudfoundry-connector/src/test/resources/META-INF/services/org.springframework.cloud.cloudfoundry.ServiceDataPostProcessor b/spring-cloud-cloudfoundry-connector/src/test/resources/META-INF/services/org.springframework.cloud.cloudfoundry.ServiceDataPostProcessor new file mode 100644 index 0000000..a571162 --- /dev/null +++ b/spring-cloud-cloudfoundry-connector/src/test/resources/META-INF/services/org.springframework.cloud.cloudfoundry.ServiceDataPostProcessor @@ -0,0 +1 @@ +org.springframework.cloud.cloudfoundry.StubServiceDataPostProcessor \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-connector/src/test/resources/org/springframework/cloud/cloudfoundry/test-credentials-post-processed.json b/spring-cloud-cloudfoundry-connector/src/test/resources/org/springframework/cloud/cloudfoundry/test-credentials-post-processed.json new file mode 100644 index 0000000..f2f6483 --- /dev/null +++ b/spring-cloud-cloudfoundry-connector/src/test/resources/org/springframework/cloud/cloudfoundry/test-credentials-post-processed.json @@ -0,0 +1,8 @@ +{ + "name": "uppercase", + "label": "mysql", + "tags": ["mysql"], + "credentials": { + "uri": "mysql://username:password@db.example.com/db" + } +} \ No newline at end of file diff --git a/spring-cloud-core/src/test/java/org/springframework/cloud/CloudTestUtil.java b/spring-cloud-core/src/test/java/org/springframework/cloud/CloudTestUtil.java index bded696..6a7eb14 100644 --- a/spring-cloud-core/src/test/java/org/springframework/cloud/CloudTestUtil.java +++ b/spring-cloud-core/src/test/java/org/springframework/cloud/CloudTestUtil.java @@ -90,14 +90,14 @@ public class CloudTestUtil { public StubServiceInfo(String id, String host, int port, String username, String password) { super(id, "stub", host, port, username, password, null); } - - // To test the scenario, where the name attribute of a property is explicitly specified + + // To test the scenario, where the name attribute of a property is explicitly specified @ServiceProperty(name="bar") public String getFoo() { return "foo"; } } - + public static class StubCompositeServiceInfo implements CompositeServiceInfo { private String id; private List constituents;