.
\ No newline at end of file
diff --git a/heroku-connector/pom.xml b/heroku-connector/pom.xml
new file mode 100644
index 0000000..951acb4
--- /dev/null
+++ b/heroku-connector/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+ org.springframework.cloud
+ heroku-connector
+ 1.0.0.CI-SNAPSHOT
+ jar
+ Spring-Cloud Heroku Connector
+ http://www.springframework.org
+
+
+
+
+ 4.11
+ 1.8.5
+
+
+
+ org.springframework.cloud
+ spring-service-connector
+ 1.0.0.CI-SNAPSHOT
+
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+ org.mockito
+ mockito-all
+ ${mockito.version}
+ test
+
+
+ log4j
+ log4j
+ 1.2.14
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 1.6
+ 1.6
+
+
+
+
+
+
diff --git a/heroku-connector/src/main/java/org/springframework/cloud/heroku/ApplicationInstanceInfoCreator.java b/heroku-connector/src/main/java/org/springframework/cloud/heroku/ApplicationInstanceInfoCreator.java
new file mode 100644
index 0000000..edb3e03
--- /dev/null
+++ b/heroku-connector/src/main/java/org/springframework/cloud/heroku/ApplicationInstanceInfoCreator.java
@@ -0,0 +1,43 @@
+package org.springframework.cloud.heroku;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import org.springframework.cloud.app.ApplicationInstanceInfo;
+import org.springframework.cloud.app.BasicApplicationInstanceInfo;
+
+/**
+ * Application instance info creator.
+ *
+ * Relies on SPRING_CLOUD_APP_NAME environment being set (using commands such as
+ * heroku config:add SPRING_CLOUD_APP_NAME=myappname --app myappname
+ *
+ * @author Ramnivas Laddad
+ *
+ */
+public class ApplicationInstanceInfoCreator {
+ private static Logger logger = Logger.getLogger(ApplicationInstanceInfoCreator.class.getName());
+
+ private EnvironmentAccessor environment;
+
+ public ApplicationInstanceInfoCreator(EnvironmentAccessor environmentAccessor) {
+ this.environment = environmentAccessor;
+ }
+
+ public ApplicationInstanceInfo createApplicationInstanceInfo() {
+ String appname = environment.getValue("SPRING_CLOUD_APP_NAME");
+ if (appname == null) {
+ logger.warning("Environment variable SPRING_CLOUD_APP_NAME not set. App name set to ");
+ appname = "";
+ }
+
+ String dyno = environment.getValue("DYNO");
+
+ Map appProperties = new HashMap();
+ appProperties.put("port", environment.getValue("PORT"));
+ appProperties.put("host", environment.getHost());
+
+ return new BasicApplicationInstanceInfo(dyno, appname, appProperties);
+ }
+}
diff --git a/heroku-connector/src/main/java/org/springframework/cloud/heroku/EnvironmentAccessor.java b/heroku-connector/src/main/java/org/springframework/cloud/heroku/EnvironmentAccessor.java
new file mode 100644
index 0000000..ec59351
--- /dev/null
+++ b/heroku-connector/src/main/java/org/springframework/cloud/heroku/EnvironmentAccessor.java
@@ -0,0 +1,35 @@
+package org.springframework.cloud.heroku;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Map;
+
+import org.springframework.cloud.CloudException;
+
+/**
+ * Environment available to the deployed app.
+ *
+ * @author Ramnivas Laddad
+ */
+public class EnvironmentAccessor {
+
+ public Map getEnv() {
+ return System.getenv();
+ }
+
+ public String getValue(String key) {
+ return System.getenv(key);
+ }
+
+ public String getPropertyValue(String key) {
+ return System.getProperty(key);
+ }
+
+ public String getHost() {
+ try {
+ return InetAddress.getLocalHost().getHostAddress();
+ } catch (UnknownHostException ex) {
+ throw new CloudException(ex);
+ }
+ }
+}
diff --git a/heroku-connector/src/main/java/org/springframework/cloud/heroku/HerokuConnector.java b/heroku-connector/src/main/java/org/springframework/cloud/heroku/HerokuConnector.java
new file mode 100644
index 0000000..ef0792a
--- /dev/null
+++ b/heroku-connector/src/main/java/org/springframework/cloud/heroku/HerokuConnector.java
@@ -0,0 +1,94 @@
+package org.springframework.cloud.heroku;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.cloud.AbstractCloudConnector;
+import org.springframework.cloud.CloudException;
+import org.springframework.cloud.ServiceInfoCreator;
+import org.springframework.cloud.app.ApplicationInstanceInfo;
+import org.springframework.cloud.service.ServiceInfo;
+
+/**
+ * Implementation of CloudConnector for Heroku
+ *
+ * Currently support only the Postgres service.
+ *
+ * @author Ramnivas Laddad
+ *
+ */
+public class HerokuConnector extends AbstractCloudConnector {
+
+ private EnvironmentAccessor environment = new EnvironmentAccessor();
+ private ApplicationInstanceInfoCreator applicationInstanceInfoCreator
+ = new ApplicationInstanceInfoCreator(environment);
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public HerokuConnector() {
+ super((Class) HerokuServiceInfoCreator.class);
+ }
+
+ @Override
+ public boolean isInMatchingCloud() {
+ return environment.getValue("DYNO") != null;
+ }
+
+ @Override
+ public ApplicationInstanceInfo getApplicationInstanceInfo() {
+ try {
+ return applicationInstanceInfoCreator.createApplicationInstanceInfo();
+ } catch (Exception e) {
+ throw new CloudException(e);
+ }
+ }
+
+ @Override
+ public List getServiceInfos() {
+ List serviceInfos = new ArrayList();
+ for (Map.Entry serviceData : getServicesData().entrySet()) {
+ serviceInfos.add(getServiceInfo(serviceData));
+ }
+
+ return serviceInfos;
+ }
+
+ /* package for testing purpose */
+ void setCloudEnvironment(EnvironmentAccessor environment) {
+ this.environment = environment;
+ this.applicationInstanceInfoCreator = new ApplicationInstanceInfoCreator(environment);
+ }
+
+ private ServiceInfo getServiceInfo(Map.Entry serviceData) {
+ for (ServiceInfoCreator> serviceInfoCreator : serviceInfoCreators) {
+ if (serviceInfoCreator.accept(serviceData)) {
+ return serviceInfoCreator.createServiceInfo(serviceData);
+ }
+ }
+
+ throw new CloudException("No suitable service info creator found");
+ }
+
+ /**
+ * Return object representation of the bound services
+ *
+ * Returns map whose key is the env key and value is the associated url
+ *
+ * @return
+ */
+ private Map getServicesData() {
+ Map serviceData = new HashMap();
+
+ Map env = environment.getEnv();
+
+ for (Map.Entry envEntry : env.entrySet()) {
+ if (envEntry.getKey().startsWith("HEROKU_POSTGRESQL_")) {
+ serviceData.put(envEntry.getKey(), envEntry.getValue());
+ }
+ }
+
+ return serviceData;
+ }
+
+}
diff --git a/heroku-connector/src/main/java/org/springframework/cloud/heroku/HerokuServiceInfoCreator.java b/heroku-connector/src/main/java/org/springframework/cloud/heroku/HerokuServiceInfoCreator.java
new file mode 100644
index 0000000..695b30b
--- /dev/null
+++ b/heroku-connector/src/main/java/org/springframework/cloud/heroku/HerokuServiceInfoCreator.java
@@ -0,0 +1,27 @@
+package org.springframework.cloud.heroku;
+
+import java.util.Map;
+
+import org.springframework.cloud.ServiceInfoCreator;
+import org.springframework.cloud.service.ServiceInfo;
+
+/**
+ *
+ * @author Ramnivas Laddad
+ *
+ */
+public abstract class HerokuServiceInfoCreator implements ServiceInfoCreator {
+
+ private String urlProtocol;
+
+ public HerokuServiceInfoCreator(String urlProtocol) {
+ this.urlProtocol = urlProtocol;
+ }
+
+ public boolean accept(Object serviceData) {
+ @SuppressWarnings("unchecked")
+ Map.Entry serviceDataEntry = (Map.Entry)serviceData;
+
+ return serviceDataEntry.getValue().toString().startsWith(urlProtocol + "://");
+ }
+}
diff --git a/heroku-connector/src/main/java/org/springframework/cloud/heroku/PostgresqlServiceInfoCreator.java b/heroku-connector/src/main/java/org/springframework/cloud/heroku/PostgresqlServiceInfoCreator.java
new file mode 100644
index 0000000..177f013
--- /dev/null
+++ b/heroku-connector/src/main/java/org/springframework/cloud/heroku/PostgresqlServiceInfoCreator.java
@@ -0,0 +1,20 @@
+package org.springframework.cloud.heroku;
+
+import org.springframework.cloud.service.common.PostgresqlServiceInfo;
+
+/**
+ *
+ * @author Ramnivas Laddad
+ *
+ */
+public class PostgresqlServiceInfoCreator extends RelationalServiceInfoCreator {
+
+ public PostgresqlServiceInfoCreator() {
+ super("postgres");
+ }
+
+ @Override
+ public PostgresqlServiceInfo createServiceInfo(String id, String uri) {
+ return new PostgresqlServiceInfo(id, uri);
+ }
+}
diff --git a/heroku-connector/src/main/java/org/springframework/cloud/heroku/RelationalServiceInfoCreator.java b/heroku-connector/src/main/java/org/springframework/cloud/heroku/RelationalServiceInfoCreator.java
new file mode 100644
index 0000000..57ed66a
--- /dev/null
+++ b/heroku-connector/src/main/java/org/springframework/cloud/heroku/RelationalServiceInfoCreator.java
@@ -0,0 +1,26 @@
+package org.springframework.cloud.heroku;
+
+import java.util.Map;
+
+import org.springframework.cloud.service.common.RelationalServiceInfo;
+
+/**
+ *
+ * @author Ramnivas Laddad
+ *
+ */
+public abstract class RelationalServiceInfoCreator extends HerokuServiceInfoCreator {
+
+ public RelationalServiceInfoCreator(String urlProtocol) {
+ super(urlProtocol);
+ }
+
+ public abstract SI createServiceInfo(String id, String uri);
+
+ public SI createServiceInfo(Object serviceData) {
+ @SuppressWarnings("unchecked")
+ Map.Entry serviceDataEntry = (Map.Entry)serviceData;
+
+ return createServiceInfo(serviceDataEntry.getKey(), serviceDataEntry.getValue());
+ }
+}
diff --git a/heroku-connector/src/main/resources/META-INF/services/org.springframework.cloud.CloudConnector b/heroku-connector/src/main/resources/META-INF/services/org.springframework.cloud.CloudConnector
new file mode 100644
index 0000000..c017a8e
--- /dev/null
+++ b/heroku-connector/src/main/resources/META-INF/services/org.springframework.cloud.CloudConnector
@@ -0,0 +1 @@
+org.springframework.cloud.heroku.HerokuConnector
\ No newline at end of file
diff --git a/heroku-connector/src/main/resources/META-INF/services/org.springframework.cloud.heroku.HerokuServiceInfoCreator b/heroku-connector/src/main/resources/META-INF/services/org.springframework.cloud.heroku.HerokuServiceInfoCreator
new file mode 100644
index 0000000..8a4a0f9
--- /dev/null
+++ b/heroku-connector/src/main/resources/META-INF/services/org.springframework.cloud.heroku.HerokuServiceInfoCreator
@@ -0,0 +1 @@
+org.springframework.cloud.heroku.PostgresqlServiceInfoCreator
diff --git a/heroku-connector/src/test/java/org/springframework/cloud/heroku/HerokuConnectorTest.java b/heroku-connector/src/test/java/org/springframework/cloud/heroku/HerokuConnectorTest.java
new file mode 100644
index 0000000..e418389
--- /dev/null
+++ b/heroku-connector/src/test/java/org/springframework/cloud/heroku/HerokuConnectorTest.java
@@ -0,0 +1,103 @@
+package org.springframework.cloud.heroku;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+import static org.springframework.cloud.heroku.HerokuConnectorTestHelper.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.cloud.service.ServiceInfo;
+import org.springframework.cloud.service.common.PostgresqlServiceInfo;
+
+/**
+ *
+ * @author Ramnivas Laddad
+ *
+ */
+public class HerokuConnectorTest {
+ private HerokuConnector testCloudConnector = new HerokuConnector();
+ @Mock EnvironmentAccessor mockEnvironment;
+
+ private static final String host = "10.20.30.40";
+ private static final int port = 1234;
+ private static String username = "myuser";
+ private static final String password = "mypass";
+
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ testCloudConnector.setCloudEnvironment(mockEnvironment);
+ }
+
+ @Test
+ public void isInMatchingEnvironment() {
+ when(mockEnvironment.getValue("DYNO")).thenReturn("web.1");
+ assertTrue(testCloudConnector.isInMatchingCloud());
+
+ when(mockEnvironment.getValue("DYNO")).thenReturn(null);
+ assertFalse(testCloudConnector.isInMatchingCloud());
+ }
+
+ @Test
+ public void postgresqlServiceCreation() {
+ Map env = new HashMap();
+ String postgresUrl = createPostgresUrl(host, port, "db", username, password);
+ env.put("HEROKU_POSTGRESQL_YELLOW_URL", postgresUrl);
+ when(mockEnvironment.getEnv()).thenReturn(env);
+
+ List serviceInfos = testCloudConnector.getServiceInfos();
+ ServiceInfo serviceInfo = getServiceInfo(serviceInfos, "HEROKU_POSTGRESQL_YELLOW_URL");
+ assertNotNull(serviceInfo);
+ assertTrue(serviceInfo instanceof PostgresqlServiceInfo);
+ PostgresqlServiceInfo postgresqlServiceInfo = (PostgresqlServiceInfo)serviceInfo;
+ assertEquals(host, postgresqlServiceInfo.getHost());
+ assertEquals(port, postgresqlServiceInfo.getPort());
+ assertEquals(username, postgresqlServiceInfo.getUserName());
+ assertEquals(password, postgresqlServiceInfo.getPassword());
+ assertEquals("jdbc:" + postgresUrl, postgresqlServiceInfo.getJdbcUrl());
+ }
+
+ @Test
+ public void applicationInstanceInfo() {
+ when(mockEnvironment.getValue("SPRING_CLOUD_APP_NAME")).thenReturn("myapp");
+ when(mockEnvironment.getValue("DYNO")).thenReturn("web.1");
+ when(mockEnvironment.getValue("PORT")).thenReturn(Integer.toString(port));
+ when(mockEnvironment.getHost()).thenReturn(host);
+
+ assertEquals("myapp", testCloudConnector.getApplicationInstanceInfo().getAppId());
+ assertEquals("web.1", testCloudConnector.getApplicationInstanceInfo().getInstanceId());
+ Map appProps = testCloudConnector.getApplicationInstanceInfo().getProperties();
+ assertEquals(host, appProps.get("host"));
+ assertEquals(Integer.toString(port), appProps.get("port"));
+ }
+
+ @Test
+ public void applicationInstanceInfoNoSpringCloudAppName() {
+ when(mockEnvironment.getValue("DYNO")).thenReturn("web.1");
+ when(mockEnvironment.getValue("PORT")).thenReturn(Integer.toString(port));
+ when(mockEnvironment.getHost()).thenReturn(host);
+ assertEquals("", testCloudConnector.getApplicationInstanceInfo().getAppId());
+ assertEquals("web.1", testCloudConnector.getApplicationInstanceInfo().getInstanceId());
+ }
+
+ private static ServiceInfo getServiceInfo(List serviceInfos, String serviceId) {
+ for (ServiceInfo serviceInfo : serviceInfos) {
+ if (serviceInfo.getId().equals(serviceId)) {
+ return serviceInfo;
+ }
+ }
+ return null;
+ }
+
+
+}
diff --git a/heroku-connector/src/test/java/org/springframework/cloud/heroku/HerokuConnectorTestHelper.java b/heroku-connector/src/test/java/org/springframework/cloud/heroku/HerokuConnectorTestHelper.java
new file mode 100644
index 0000000..1a598b6
--- /dev/null
+++ b/heroku-connector/src/test/java/org/springframework/cloud/heroku/HerokuConnectorTestHelper.java
@@ -0,0 +1,19 @@
+package org.springframework.cloud.heroku;
+
+
+/**
+ *
+ * @author Ramnivas Laddad
+ *
+ */
+public class HerokuConnectorTestHelper {
+ public static String createPostgresUrl(String host, int port, String database, String username, String password) {
+ String template = "postgres://$username:$password@$host:$port/$database";
+
+ return template.replace("$username", username).
+ replace("$password", password).
+ replace("$host", host).
+ replace("$port", Integer.toString(port)).
+ replace("$database", database);
+ }
+}
diff --git a/heroku-connector/src/test/resources/log4j.properties b/heroku-connector/src/test/resources/log4j.properties
new file mode 100644
index 0000000..11492f1
--- /dev/null
+++ b/heroku-connector/src/test/resources/log4j.properties
@@ -0,0 +1,6 @@
+log4j.rootCategory=INFO, stdout
+
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %40.40c:%4L - %m%n
+
diff --git a/pom.xml b/pom.xml
index f3921dc..00a30f9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,5 +12,6 @@
core
spring-service-connector
cloudfoundry-connector
+ heroku-connector
diff --git a/spring-service-connector/pom.xml b/spring-service-connector/pom.xml
index c3a3c31..73baa2a 100644
--- a/spring-service-connector/pom.xml
+++ b/spring-service-connector/pom.xml
@@ -15,7 +15,6 @@
]]>
- true
3.0.7.RELEASE
6.0.29