Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ public String createJdbcUrl(CfService cfService) {
@Override
public String buildJdbcUrlFromUriField(CfCredentials cfCredentials) {
UriInfo uriInfo = cfCredentials.getUriInfo(MYSQL_SCHEME);
return String.format("%s%s://%s%s/%s%s%s", JDBC_PREFIX, MYSQL_SCHEME,
uriInfo.getHost(), uriInfo.formatPort(), uriInfo.getPath(),
return String.format("%s%s://%s/%s%s%s", JDBC_PREFIX, MYSQL_SCHEME,
uriInfo.getHostAndPort(), uriInfo.getPath(),
uriInfo.formatUserNameAndPasswordQuery(), uriInfo.formatQuery());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ public boolean isDatabaseService(CfService cfService) {
@Override
public String buildJdbcUrlFromUriField(CfCredentials cfCredentials) {
UriInfo uriInfo = cfCredentials.getUriInfo(POSTGRES_JDBC_SCHEME);
return String.format("%s%s://%s%s/%s%s%s", JDBC_PREFIX, POSTGRES_JDBC_SCHEME,
uriInfo.getHost(), uriInfo.formatPort(), uriInfo.getPath(),
return String.format("%s%s://%s/%s%s%s", JDBC_PREFIX, POSTGRES_JDBC_SCHEME,
uriInfo.getHostAndPort(), uriInfo.getPath(),
uriInfo.formatUserNameAndPasswordQuery(), uriInfo.formatQuery());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ public void mysqlServiceCreation() {
"No unique database service matching by name [mysql.*] was found. Matching service names are [mysql-1, mysql-2]");
}

@Test
public void mysqlServiceCreationMultiHost() {
String name = "database-1";

mockVcapServices(getServicesPayload(getMysqlServicePayloadMultiHost("mysql-1",
hostname, port, username, password, name)));

CfJdbcEnv cfJdbcEnv = new CfJdbcEnv();
CfJdbcService cfJdbcService = cfJdbcEnv.findJdbcServiceByName("mysql-1");

assertThat(cfJdbcService.getJdbcUrl()).isEqualTo(String.format(
"jdbc:mysql://host1:3306,host2:3306/%s?user=%s&password=%s", name,
UriInfo.urlEncode(username), UriInfo.urlEncode(password)));
assertThat(cfJdbcService.getUsername()).isEqualTo(username);
assertThat(cfJdbcService.getPassword()).isEqualTo(password);
}

@Test
public void mysqlServiceCreationWithLabelNoTags() {
String name1 = "database-1";
Expand Down Expand Up @@ -343,6 +360,12 @@ private String getMysqlServicePayload(String serviceName, String hostname, int p
user, password, name);
}

private String getMysqlServicePayloadMultiHost(String serviceName, String hostname,
int port, String user, String password, String name) {
return getTemplatedPayload("test-mysql-info-multi-host.json", serviceName, hostname,
port, user, password, name);
}

private String getMysqlServicePayloadWithLabelNoTags(String serviceName,
String hostname, int port, String user, String password, String name) {
return getTemplatedPayload("test-mysql-info-with-label-no-tags.json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,24 @@ public void postgresqlWithSpecialCharsServiceCreation() {
passwordWithSpecialChars);
}

@Test
public void postgresqlServiceCreationMultiHost() {
String name1 = "database-1";

mockVcapServices(getServicesPayload(
getPostgresqlServicePayloadMultiHost("postgresql-1", hostname, port, username, password, name1)));

CfJdbcEnv cfJdbcEnv = new CfJdbcEnv();
CfJdbcService cfJdbcService = cfJdbcEnv.findJdbcServiceByName("postgresql-1");

assertThat(cfJdbcService.getJdbcUrl()).isEqualTo(String.format(
"jdbc:postgresql://host1:5432,host2:5432/%s?user=%s&password=%s", name1,
UriInfo.urlEncode(username), UriInfo.urlEncode(password)));
assertThat(cfJdbcService.getUsername()).isEqualTo(username);
assertThat(cfJdbcService.getPassword()).isEqualTo(password);
assertThat(cfJdbcService.getDriverClassName()).isEqualTo("org.postgresql.Driver");
}

@Test
public void postgresqlServiceCreationNoLabelNoTags() {
String name1 = "database-1";
Expand Down Expand Up @@ -231,6 +249,13 @@ private String getPostgresqlServicePayloadNoLabelNoTags(String serviceName,
hostname, port, user, password, name);
}

private String getPostgresqlServicePayloadMultiHost(String serviceName,
String hostname, int port,
String user, String password, String name) {
return getTemplatedPayload("test-postgresql-info-multi-host.json", serviceName,
hostname, port, user, password, name);
}

private String getPostgresqlServicePayloadWithJdbcUrl(String serviceName,
String hostname, int port,
String user, String password, String name) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "$serviceName",
"label": "cleardb",
"tags": [
"mysql"
],
"plan": "free",
"credentials": {
"uri": "mysql://$user:$password@host1:3306,host2:3306/$name"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "$serviceName",
"label": "elephantsql",
"plan": "free",
"tags": [ "postgresql" ],
"credentials": {
"uri": "postgres://$user:$password@host1:5432,host2:5432/$name"
}
}
80 changes: 75 additions & 5 deletions java-cfenv/src/main/java/io/pivotal/cfenv/core/UriInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public class UriInfo {

private String uriString;

private String hosts;

public UriInfo(String scheme, String host, int port, String username,
String password) {
this(scheme, host, port, username, password, null, null);
Expand Down Expand Up @@ -74,14 +76,67 @@ public UriInfo(String uriString) {

URI uri = getUri();
this.scheme = uri.getScheme();
this.host = uri.getHost();
this.port = uri.getPort();
this.path = parsePath(uri);
this.query = uri.getQuery();

String[] userinfo = parseUserinfo(uri);
this.userName = urlDecode(userinfo[0]);
this.password = urlDecode(userinfo[1]);
String authority = uri.getRawAuthority();
if (uri.getHost() != null) {
this.host = uri.getHost();
this.port = uri.getPort();

String[] userinfo = parseUserinfo(uri);
this.userName = urlDecode(userinfo[0]);
this.password = urlDecode(userinfo[1]);
}
else if (authority != null && authority.substring(authority.lastIndexOf('@') + 1).contains(",")) {
// A multi-host authority (e.g. postgresql's
// host1:port1,host2:port2 failover syntax) isn't valid
// server-based authority per RFC 3986, so java.net.URI parses
// it as a registry-based authority: getHost()/getPort() come
// back null/-1 even though getRawAuthority() has the full value.
// Use the raw (not percent-decoded) form so a password
// containing an encoded ',' ':' or '@' isn't mistaken for a
// delimiter.
parseAuthority(authority);
}
else {
// Some non-standard, driver-specific formats (e.g. SQL Server's
// ;property=value suffix) also fail server-based parsing but
// aren't a list of hosts either; leave host/port unset as before
// and let the caller fall back to its own parsing of uriString.
this.host = uri.getHost();
this.port = uri.getPort();
}
}

private void parseAuthority(String authority) {
String hostsPart = authority;

int at = authority.lastIndexOf('@');
if (at != -1) {
String userInfo = authority.substring(0, at);
hostsPart = authority.substring(at + 1);

String[] userPass = userInfo.split(":");
if (userPass.length != 2) {
throw new IllegalArgumentException("Bad userinfo in URI: " + uriString);
}
this.userName = urlDecode(userPass[0]);
this.password = urlDecode(userPass[1]);
}

this.hosts = hostsPart;

String firstHost = hostsPart.split(",")[0];
int colon = firstHost.lastIndexOf(':');
if (colon != -1) {
this.host = firstHost.substring(0, colon);
this.port = Integer.parseInt(firstHost.substring(colon + 1));
}
else {
this.host = firstHost;
this.port = -1;
}
}

public static String urlDecode(String s) {
Expand Down Expand Up @@ -165,6 +220,21 @@ public String formatPort() {
return "";
}

/**
* Returns the host(s) and port(s) as they should appear in the authority
* section of a URI. For a single-host URI this is equivalent to
* {@code getHost() + formatPort()}. For a multi-host URI (e.g.
* PostgreSQL's {@code host1:port1,host2:port2} failover syntax), the
* full, unmodified host list is returned since {@link #getHost()} and
* {@link #getPort()} only expose the first host.
*/
public String getHostAndPort() {
if (hosts != null) {
return hosts;
}
return getHost() + formatPort();
}

public String formatQuery() {
if (getQuery() != null) {
if (formatUserNameAndPasswordQuery().isEmpty()) {
Expand Down
67 changes: 67 additions & 0 deletions java-cfenv/src/test/java/io/pivotal/cfenv/core/UriInfoTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,73 @@ public void createWithUsernameNoPassword() {
assertThatThrownBy(() -> new UriInfo(uri)).isInstanceOf(IllegalArgumentException.class);
}

@Test
public void createMultiHostUri() {
String uri = "postgresql://joe:joes_password@host1:5432,host2:5432/big_db?p1=v1";
UriInfo uriInfo = new UriInfo(uri);

assertThat(uriInfo.getScheme()).isEqualTo("postgresql");
assertThat(uriInfo.getHost()).isEqualTo("host1");
assertThat(uriInfo.getPort()).isEqualTo(5432);
assertThat(uriInfo.getUsername()).isEqualTo("joe");
assertThat(uriInfo.getPassword()).isEqualTo("joes_password");
assertThat(uriInfo.getPath()).isEqualTo("big_db");
assertThat(uriInfo.getQuery()).isEqualTo("p1=v1");
assertThat(uriInfo.getHostAndPort()).isEqualTo("host1:5432,host2:5432");
assertThat(uri).isEqualTo(uriInfo.getUriString());
}

@Test
public void createMultiHostUriNonPostgresScheme() {
// The multi-host authority format isn't postgresql-specific; mysql and
// mariadb use the same host1:port1,host2:port2 syntax for failover.
String uri = "mysql://joe:joes_password@host1:3306,host2:3306/big_db";
UriInfo uriInfo = new UriInfo(uri);

assertThat(uriInfo.getScheme()).isEqualTo("mysql");
assertThat(uriInfo.getHostAndPort()).isEqualTo("host1:3306,host2:3306");
assertThat(uriInfo.getHost()).isEqualTo("host1");
assertThat(uriInfo.getPort()).isEqualTo(3306);
assertThat(uriInfo.getUsername()).isEqualTo("joe");
assertThat(uriInfo.getPassword()).isEqualTo("joes_password");
assertThat(uriInfo.getPath()).isEqualTo("big_db");
}

@Test
public void createSingleHostUriIsUnaffectedByMultiHostParsing() {
// A standard single-host authority must keep using java.net.URI's
// server-based parsing, never the comma-detection fallback.
String uri = "mysql://joe:joes_password@localhost:1527/big_db";
UriInfo uriInfo = new UriInfo(uri);

assertUriInfoEquals(uriInfo, "localhost", 1527, "joe", "joes_password", "big_db",
null);
assertThat(uriInfo.getHostAndPort()).isEqualTo("localhost:1527");
}

@Test
public void createMultiHostUriWithEncodedDelimiterInPassword() {
String uri = "postgresql://joe:pa%3Ass@host1:5432,host2:5432/big_db";
UriInfo uriInfo = new UriInfo(uri);

assertThat(uriInfo.getUsername()).isEqualTo("joe");
assertThat(uriInfo.getPassword()).isEqualTo("pa:ss");
assertThat(uriInfo.getHostAndPort()).isEqualTo("host1:5432,host2:5432");
}

@Test
public void createMultiHostUriNoUserInfo() {
String uri = "postgresql://host1:5432,host2:5432/big_db";
UriInfo uriInfo = new UriInfo(uri);

assertThat(uriInfo.getHostAndPort()).isEqualTo("host1:5432,host2:5432");
assertThat(uriInfo.getHost()).isEqualTo("host1");
assertThat(uriInfo.getPort()).isEqualTo(5432);
assertThat(uriInfo.getUsername()).isNull();
assertThat(uriInfo.getPassword()).isNull();
assertThat(uriInfo.getPath()).isEqualTo("big_db");
}

@Test
public void createWithExplicitParameters() {
String uri = "mysql://joe:joes_password@localhost:1527/big_db";
Expand Down
Loading