diff --git a/README.md b/README.md index 4f1485e..6c94216 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,26 @@ Disable Property: `org.springframework.cloud.bindings.boot.db2.enable` | `spring.r2dbc.password` | `{password}` | | `spring.r2dbc.username` | `{username}` | +### Replicated DB2 RDBMS +Type: `db2-replicated` +Disable Property: `org.springframework.cloud.bindings.boot.db2-replicated.enable` + +| Property | Value | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `spring.datasource.replicated.rw.driver-class-name` | `com.ibm.db2.jcc.DB2Driver` | +| `spring.datasource.replicated.rw.password` | `{rw-password}` | +| `spring.datasource.replicated.rw.url` | `{rw-jdbc-url}` or if not set then `jdbc:db2://{rw-host}:{rw-port}/{rw-database}` (you must have rw-host, rw-port and rw-database set or no mapping will occur) | +| `spring.datasource.replicated.rw.username` | `{rw-username}` | +| `spring.r2dbc.replicated.rw.url` | `{rw-r2dbc-url}` or if not set then `r2dbc:db2://{rw-host}:{rw-port}/{rw-database}` (you must have rw-host, rw-port and rw-database set or no mapping will occur) | +| `spring.r2dbc.replicated.rw.password` | `{rw-password}` | +| `spring.r2dbc.replicated.rw.username` | `{rw-username}` | +| `spring.datasource.replicated.ro.driver-class-name` | `com.ibm.db2.jcc.DB2Driver` | +| `spring.datasource.replicated.ro.password` | `{ro-password}` | +| `spring.datasource.replicated.ro.url` | `{ro-jdbc-url}` or if not set then `jdbc:db2://{ro-host}:{ro-port}/{ro-database}` (you must have ro-host, ro-port and ro-database set or no mapping will occur) | +| `spring.datasource.replicated.ro.username` | `{ro-username}` | +| `spring.r2dbc.replicated.ro.url` | `{ro-r2dbc-url}` or if not set then `r2dbc:db2://{ro-host}:{ro-port}/{ro-database}` (you must have ro-host, ro-port and ro- database set or no mapping will occur) | +| `spring.r2dbc.replicated.ro.password` | `{ro-password}` | +| `spring.r2dbc.replicated.ro.username` | `{ro-username}` | ### Elasticsearch Type: `elasticsearch` @@ -180,6 +200,27 @@ Disable Property: `org.springframework.cloud.bindings.boot.mysql.enable` | `spring.r2dbc.password` | `{password}` | | `spring.r2dbc.username` | `{username}` | +### Replicated MySQL RDBMS +Type: `mysql-replicated` +Disable Property: `org.springframework.cloud.bindings.boot.mysql-replicated.enable` + +| Property | Value | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `spring.datasource.replicated.rw.driver-class-name` | `org.mariadb.jdbc.Driver` or `com.mysql.cj.jdbc.Driver` depending on classpath | +| `spring.datasource.replicated.rw.password` | `{rw-password}` | +| `spring.datasource.replicated.rw.url` | `{rw-jdbc-url}` or if not set then `jdbc:mysql://{rw-host}:{rw-port}/{rw-database}` or `jdbc:mariadb://{rw-host}:{rw-port}/{rw-database}` (you must have rw-host, rw-port and rw-database set or no mapping will occur) | +| `spring.datasource.replicated.rw.username` | `{rw-username}` | +| `spring.r2dbc.replicated.rw.url` | `{rw-r2dbc-url}` or if not set then `r2dbc:mysql://{rw-host}:{rw-port}/{rw-database}` or `r2dbc:mariadb//{rw-host}:{rw-port}/{rw-database}` (you must have rw-host, rw-port and rw-database set or no mapping will occur) | +| `spring.r2dbc.replicated.rw.password` | `{rw-password}` | +| `spring.r2dbc.replicated.rw.username` | `{rw-username}` | +| `spring.datasource.replicated.ro.driver-class-name` | `org.mariadb.jdbc.Driver` or `com.mysql.cj.jdbc.Driver` depending on classpath | +| `spring.datasource.replicated.ro.password` | `{ro-password}` | +| `spring.datasource.replicated.ro.url` | `{ro-jdbc-url}` or if not set then `jdbc:mysql://{ro-host}:{ro-port}/{ro-database}` or `jdbc:mariadb://{ro-host}:{ro-port}/{ro-database}` (you must have ro-host, ro-port and ro-database set or no mapping will occur) | +| `spring.datasource.replicated.ro.username` | `{ro-username}` | +| `spring.r2dbc.replicated.ro.url` | `{ro-r2dbc-url}` or if not set then `r2dbc:mysql://{ro-host}:{ro-port}/{ro-database}` or `r2dbc:mariadb//{ro-host}:{ro-port}/{ro-database}` (you must have ro-host, ro-port and ro- database set or no mapping will occur) | +| `spring.r2dbc.replicated.ro.password` | `{ro-password}` | +| `spring.r2dbc.replicated.ro.username` | `{ro-username}` | + **Note:** Libraries on the classpath are examined for the purpose of evaluating the appropriate `jdbc` and `r2dbc` URLs. The existence of both MySQL and MariaDB libraries on the classpath is not supported and may lead to non-deterministic results. ### Neo4J @@ -208,6 +249,28 @@ Disable Property: `org.springframework.cloud.bindings.boot.oracle.enable` | `spring.r2dbc.password` | `{password}` | | `spring.r2dbc.username` | `{username}` | +### Replicated Oracle RDBMS +Type: `oracle-replicated` +Disable Property: `org.springframework.cloud.bindings.boot.oracle-replicated.enable` + +| Property | Value | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `spring.datasource.replicated.rw.driver-class-name` | `oracle.jdbc.OracleDriver` | +| `spring.datasource.replicated.rw.password` | `{rw-password}` | +| `spring.datasource.replicated.rw.url` | `{rw-jdbc-url}` or if not set then `jdbc:oracle://{rw-host}:{rw-port}/{rw-database}` (you must have rw-host, rw-port and rw-database set or no mapping will occur) | +| `spring.datasource.replicated.rw.username` | `{rw-username}` | +| `spring.r2dbc.replicated.rw.url` | `{rw-r2dbc-url}` or if not set then `r2dbc:oracle://{rw-host}:{rw-port}/{rw-database}` (you must have rw-host, rw-port and rw-database set or no mapping will occur) | +| `spring.r2dbc.replicated.rw.password` | `{rw-password}` | +| `spring.r2dbc.replicated.rw.username` | `{rw-username}` | +| `spring.datasource.replicated.ro.driver-class-name` | `oracle.jdbc.OracleDriver` | +| `spring.datasource.replicated.ro.password` | `{ro-password}` | +| `spring.datasource.replicated.ro.url` | `{ro-jdbc-url}` or if not set then `jdbc:oracle://{ro-host}:{ro-port}/{ro-database}` (you must have ro-host, ro-port and ro-database set or no mapping will occur) | +| `spring.datasource.replicated.ro.username` | `{ro-username}` | +| `spring.r2dbc.replicated.ro.url` | `{ro-r2dbc-url}` or if not set then `r2dbc:oracle://{ro-host}:{ro-port}/{ro-database}` (you must have ro-host, ro-port and ro- database set or no mapping will occur) | +| `spring.r2dbc.replicated.ro.password` | `{ro-password}` | +| `spring.r2dbc.replicated.ro.username` | `{ro-username}` | + + ### PostgreSQL RDBMS Type: `postgresql` Disable Property: `org.springframework.cloud.bindings.boot.postgresql.enable` @@ -222,6 +285,27 @@ Disable Property: `org.springframework.cloud.bindings.boot.postgresql.enable` | `spring.r2dbc.password` | `{password}` | | `spring.r2dbc.username` | `{username}` | +### Replicated PostgreSQL RDBMS +Type: `postgresql-replicated` +Disable Property: `org.springframework.cloud.bindings.boot.postgresql-replicated.enable` + +| Property | Value | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `spring.datasource.replicated.rw.driver-class-name` | `org.postgresql.Driver` | +| `spring.datasource.replicated.rw.password` | `{rw-password}` | +| `spring.datasource.replicated.rw.url` | `{rw-jdbc-url}` or if not set then `jdbc:postgresql://{rw-host}:{rw-port}/{rw-database}` (you must have rw-host, rw-port and rw-database set or no mapping will occur) | +| `spring.datasource.replicated.rw.username` | `{rw-username}` | +| `spring.r2dbc.replicated.rw.url` | `{rw-r2dbc-url}` or if not set then `r2dbc:postgresql://{rw-host}:{rw-port}/{rw-database}` (you must have rw-host, rw-port and rw-database set or no mapping will occur) | +| `spring.r2dbc.replicated.rw.password` | `{rw-password}` | +| `spring.r2dbc.replicated.rw.username` | `{rw-username}` | +| `spring.datasource.replicated.ro.driver-class-name` | `org.postgresql.Driver` | +| `spring.datasource.replicated.ro.password` | `{ro-password}` | +| `spring.datasource.replicated.ro.url` | `{ro-jdbc-url}` or if not set then `jdbc:postgresql://{ro-host}:{ro-port}/{ro-database}` (you must have ro-host, ro-port and ro-database set or no mapping will occur) | +| `spring.datasource.replicated.ro.username` | `{ro-username}` | +| `spring.r2dbc.replicated.ro.url` | `{ro-r2dbc-url}` or if not set then `r2dbc:postgresql://{ro-host}:{ro-port}/{ro-database}` (you must have ro-host, ro-port and ro- database set or no mapping will occur) | +| `spring.r2dbc.replicated.ro.password` | `{ro-password}` | +| `spring.r2dbc.replicated.ro.username` | `{ro-username}` | + ### RabbitMQ Type: `rabbitmq` Disable Property: `org.springframework.cloud.bindings.boot.rabbitmq.enable` @@ -269,6 +353,27 @@ Disable Property: `org.springframework.cloud.bindings.boot.hana.enable` | `spring.r2dbc.password` | `{password}` | | `spring.r2dbc.username` | `{username}` | +### Replicated SAP Hana RDBMS +Type: `hana-replicated` +Disable Property: `org.springframework.cloud.bindings.boot.hana-replicated.enable` + +| Property | Value | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `spring.datasource.replicated.rw.driver-class-name` | `com.sap.db.jdbc.Driver` | +| `spring.datasource.replicated.rw.password` | `{rw-password}` | +| `spring.datasource.replicated.rw.url` | `{rw-jdbc-url}` or if not set then `jdbc:sap://{rw-host}:{rw-port}/{rw-database}` (you must have rw-host, rw-port and rw-database set or no mapping will occur) | +| `spring.datasource.replicated.rw.username` | `{rw-username}` | +| `spring.r2dbc.replicated.rw.url` | `{rw-r2dbc-url}` or if not set then `r2dbc:sap://{rw-host}:{rw-port}/{rw-database}` (you must have rw-host, rw-port and rw-database set or no mapping will occur) | +| `spring.r2dbc.replicated.rw.password` | `{rw-password}` | +| `spring.r2dbc.replicated.rw.username` | `{rw-username}` | +| `spring.datasource.replicated.ro.driver-class-name` | `com.sap.db.jdbc.Driver` | +| `spring.datasource.replicated.ro.password` | `{ro-password}` | +| `spring.datasource.replicated.ro.url` | `{ro-jdbc-url}` or if not set then `jdbc:sap://{ro-host}:{ro-port}/{ro-database}` (you must have ro-host, ro-port and ro-database set or no mapping will occur) | +| `spring.datasource.replicated.ro.username` | `{ro-username}` | +| `spring.r2dbc.replicated.ro.url` | `{ro-r2dbc-url}` or if not set then `r2dbc:sap://{ro-host}:{ro-port}/{ro-database}` (you must have ro-host, ro-port and ro- database set or no mapping will occur) | +| `spring.r2dbc.replicated.ro.password` | `{ro-password}` | +| `spring.r2dbc.replicated.ro.username` | `{ro-username}` | + ## SCS Config Server Type: `config` Disable Property: `org.springframework.cloud.bindings.boot.config.enable` @@ -342,6 +447,27 @@ Disable Property: `org.springframework.cloud.bindings.boot.sqlserver.enable` | `spring.r2dbc.password` | `{password}` | | `spring.r2dbc.username` | `{username}` | +### Replicated SQLServer RDBMS +Type: `sqlserver-replicated` +Disable Property: `org.springframework.cloud.bindings.boot.sqlserver-replicated.enable` + +| Property | Value | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `spring.datasource.replicated.rw.driver-class-name` | `com.microsoft.sqlserver.jdbc.SQLServerDriver` | +| `spring.datasource.replicated.rw.password` | `{rw-password}` | +| `spring.datasource.replicated.rw.url` | `{rw-jdbc-url}` or if not set then `jdbc:sqlserver://{rw-host}:{rw-port}/{rw-database}` (you must have rw-host, rw-port and rw-database set or no mapping will occur) | +| `spring.datasource.replicated.rw.username` | `{rw-username}` | +| `spring.r2dbc.replicated.rw.url` | `{rw-r2dbc-url}` or if not set then `r2dbc:sqlserver://{rw-host}:{rw-port}/{rw-database}` (you must have rw-host, rw-port and rw-database set or no mapping will occur) | +| `spring.r2dbc.replicated.rw.password` | `{rw-password}` | +| `spring.r2dbc.replicated.rw.username` | `{rw-username}` | +| `spring.datasource.replicated.ro.driver-class-name` | `com.microsoft.sqlserver.jdbc.SQLServerDriver` | +| `spring.datasource.replicated.ro.password` | `{ro-password}` | +| `spring.datasource.replicated.ro.url` | `{ro-jdbc-url}` or if not set then `jdbc:sqlserver://{ro-host}:{ro-port}/{ro-database}` (you must have ro-host, ro-port and ro-database set or no mapping will occur) | +| `spring.datasource.replicated.ro.username` | `{ro-username}` | +| `spring.r2dbc.replicated.ro.url` | `{ro-r2dbc-url}` or if not set then `r2dbc:sqlserver://{ro-host}:{ro-port}/{ro-database}` (you must have ro-host, ro-port and ro- database set or no mapping will occur) | +| `spring.r2dbc.replicated.ro.password` | `{ro-password}` | +| `spring.r2dbc.replicated.ro.username` | `{ro-username}` | + ### Vault Type: `vault` diff --git a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/AbstractPostgreSQLBindingsPropertiesProcessor.java b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/AbstractPostgreSQLBindingsPropertiesProcessor.java new file mode 100644 index 0000000..77b3a86 --- /dev/null +++ b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/AbstractPostgreSQLBindingsPropertiesProcessor.java @@ -0,0 +1,129 @@ +/* + * Copyright 2024 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.nio.file.FileSystems; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.cloud.bindings.Binding; + +public abstract class AbstractPostgreSQLBindingsPropertiesProcessor implements BindingsPropertiesProcessor { + + /** + * sslmode determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. + */ + public static final String SSL_MODE = "sslmode"; + /** + * sslrootcert specifies the name of a file containing SSL certificate authority (CA) certificate(s). + */ + public static final String SSL_ROOT_CERT = "sslrootcert"; + /** + * options Specifies command-line options to send to the server at connection start. + * CockroachDB uses this to pass in cluster routing id + */ + public static final String OPTIONS = "options"; + + /** + * Returns a concatenated list of options parameters defined in the bound file `options` in the format specified in + * PostgreSQL Doc. + *

+ * CockroachDB, which shares the same 'postgresql://' protocol as PostgreSQL, has customized options to meet its + * distributed database nature. + * Refer to Client Connection Parameters. + */ + protected String buildDbOptions(Binding binding) { + String options = binding.getSecret().getOrDefault(getDBOptionSecretField(), ""); + String crdbOption = ""; + List dbOptions = new ArrayList<>(); + if (!options.equals("")) { + String[] allOpts = options.split("&"); + for (String o : allOpts) { + String[] keyval = o.split("="); + if (keyval.length != 2 || keyval[0].length() == 0 || keyval[1].length() == 0) { + continue; + } + if (keyval[0].equals("--cluster")) { + crdbOption = keyval[0] + "=" + keyval[1]; + } else { + dbOptions.add("-c " + keyval[0] + "=" + keyval[1]); + } + } + } + String combinedOptions = crdbOption; + if (dbOptions.size() > 0) { + String otherOpts = String.join(" ", dbOptions); + if (!combinedOptions.equals("")) { + combinedOptions = combinedOptions + " " + otherOpts; + } else { + combinedOptions = otherOpts; + } + } + if (!"".equals(combinedOptions)) { + combinedOptions = "options=" + combinedOptions; + } + return combinedOptions; + } + + /** + * Returns a concatenated string of all ssl parameters for enabling one-way TLS (PostgreSQL certifies itself) + * Refer to PostgreSQL Doc + */ + protected String buildSslModeParam(Binding binding) { + //process ssl params + //https://www.postgresql.org/docs/14/libpq-connect.html + String sslmode = binding.getSecret().getOrDefault(getSSLModeSecretField(), ""); + String sslRootCert = binding.getSecret().getOrDefault(getSSLRootCertSecretField(), ""); + StringBuilder sslparam = new StringBuilder(); + if (!"".equals(sslmode)) { + sslparam.append(SSL_MODE).append("=").append(sslmode); + } + if (!"".equals(sslRootCert)) { + if (!"".equals(sslmode)) { + sslparam.append("&"); + } + sslparam.append(SSL_ROOT_CERT).append("=") + .append(binding.getPath()).append(FileSystems.getDefault().getSeparator()) + .append(sslRootCert); + } + return sslparam.toString(); + } + + /** + * name of the options field; override in concrete classes is field name is different + */ + protected String getDBOptionSecretField() + { + return OPTIONS; + } + + /** + * name of the ssl mode field; override in concrete classes is field name is different + */ + protected String getSSLModeSecretField() + { + return SSL_MODE; + } + + /** + * name of the ssl root cert field; override in concrete classes is field name is different + */ + protected String getSSLRootCertSecretField() + { + return SSL_ROOT_CERT; + } +} diff --git a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/AbstractReplicatedDataSource.java b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/AbstractReplicatedDataSource.java new file mode 100644 index 0000000..4168eb7 --- /dev/null +++ b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/AbstractReplicatedDataSource.java @@ -0,0 +1,114 @@ +/* + * Copyright 2024 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 static org.springframework.cloud.bindings.boot.Guards.isTypeEnabled; + +import java.util.Map; + +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.core.env.Environment; + +/** + * Abstract class for replicated datasources processors that implement a common properties pattern. + */ +public abstract class AbstractReplicatedDataSource implements BindingsPropertiesProcessor { + + /** + * Read/write function field + */ + public static final String RW_FUNCTION = "rw"; + + /** + * Read only function field + */ + public static final String RO_FUNCTION = "ro"; + + /** + * Template for a base JDBC properties + */ + public static final String JDBC_BASE_TEMPLATE = "spring.datasource.replicated.%s"; + + /** + * Template for a base R2DBC properties + */ + public static final String R2DBC_BASE_TEMPLATE = "spring.r2dbc.replicated.%s"; + + /** + * Template for function based JDBC properties + */ + public static final String JDBC_PROPERTY_TEMPLATE = "spring.datasource.replicated.%s.%s"; + + /** + * Template for function based R2DBC properties + */ + public static final String R2DBC_PROPERTY_TEMPLATE = "spring.r2dbc.replicated.%s.%s"; + + @Override + public void process(Environment environment, Bindings bindings, Map properties) { + if (!isTypeEnabled(environment, getType())) { + return; + } + + bindings.filterBindings(getType()).forEach(binding -> { + + properties.put(String.format(JDBC_BASE_TEMPLATE, "name"), binding.getName()); + + properties.put(String.format(R2DBC_BASE_TEMPLATE, "name"), binding.getName()); + + final var map = new MapMapper(binding.getSecret(), properties); + + buildProperties(map, properties, binding, RW_FUNCTION); + + buildProperties(map, properties, binding, RO_FUNCTION); + + }); + } + + /** + * Format a JDBC property + */ + protected String formatJDBCTemplate(String fnc, String field) + { + return String.format(JDBC_PROPERTY_TEMPLATE, fnc, field); + } + + /** + * Format an R2DBC property + */ + protected String formatR2DBCTemplate(String fnc, String field) + { + return String.format(R2DBC_PROPERTY_TEMPLATE, fnc, field); + } + + /** + * Builds the properties for a given operations function + * @param map The properties mapper + * @param properties Properties resource where new spring properties will be written to + * @param binding The current binding object + * @param fnc The operations function of the current binding + */ + protected abstract void buildProperties(MapMapper map, Map properties, Binding binding, String fnc); + + /** + * Gets the binding type of this processor. + * @return The binding type. + */ + protected abstract String getType(); + +} diff --git a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/Db2ReplicatedBindingsPropertiesProcessor.java b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/Db2ReplicatedBindingsPropertiesProcessor.java new file mode 100644 index 0000000..7580f73 --- /dev/null +++ b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/Db2ReplicatedBindingsPropertiesProcessor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024 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.Map; + +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.logging.DeferredLog; +import org.springframework.cloud.bindings.Binding; +import org.springframework.context.ApplicationListener; + +/** + * An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s of type: {@value TYPE}. + */ +public final class Db2ReplicatedBindingsPropertiesProcessor extends AbstractReplicatedDataSource + implements ApplicationListener { + + /** + * The {@link Binding} type that this processor is interested in: {@value}. + **/ + public static final String TYPE = "db2-replicated"; + + private static final DeferredLog LOG = new DeferredLog(); + + @Override + protected void buildProperties(MapMapper map, Map properties, Binding binding, String fnc) + { + + //jdbc properties + map.from(fnc + "-username").to(formatJDBCTemplate(fnc, "username")); + map.from(fnc + "-password").to(formatJDBCTemplate(fnc, "password")); + map.from(fnc + "-host", fnc + "-port", fnc + "-database").to(formatJDBCTemplate(fnc, "url"), + (host, port, database) -> String.format("jdbc:db2://%s:%s/%s", host, port, database)); + + // jdbcURL takes precedence + map.from(fnc + "-jdbc-url").to(formatJDBCTemplate(fnc, "url")); + + properties.put(formatJDBCTemplate(fnc, "driver-class-name"), "com.ibm.db2.jcc.DB2Driver"); + + //r2dbc properties + map.from(fnc + "-username").to(formatR2DBCTemplate(fnc, "username")); + map.from(fnc + "-password").to(formatR2DBCTemplate(fnc, "password")); + map.from(fnc + "-host", fnc + "-port", fnc + "-database").to(formatR2DBCTemplate(fnc, "url"), + (host, port, database) -> String.format("r2dbc:db2://%s:%s/%s", host, port, database)); + + // r2dbcURL takes precedence + map.from(fnc + "-r2dbc-url").to(formatR2DBCTemplate(fnc, "url")); + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + public void onApplicationEvent(ApplicationPreparedEvent event) { + LOG.replayTo(getClass()); + } +} diff --git a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/MySqlReplicatedBindingsPropertiesProcessor.java b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/MySqlReplicatedBindingsPropertiesProcessor.java new file mode 100644 index 0000000..410ca1e --- /dev/null +++ b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/MySqlReplicatedBindingsPropertiesProcessor.java @@ -0,0 +1,116 @@ +/* + * Copyright 2024 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.Map; + +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.logging.DeferredLog; +import org.springframework.cloud.bindings.Binding; +import org.springframework.context.ApplicationListener; + +/** + * An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s of type: {@value TYPE}. + */ +public final class MySqlReplicatedBindingsPropertiesProcessor extends AbstractReplicatedDataSource + implements ApplicationListener { + + /** + * The {@link Binding} type that this processor is interested in: {@value}. + **/ + public static final String TYPE = "mysql-replicated"; + + private static final DeferredLog LOG = new DeferredLog(); + + /** + * MySQL connection protocol constant. + */ + private static final String MYSQL_PROTOCOL = "mysql"; + + /** + * MariaDB connection protocol constant. + */ + private static final String MARIADB_PROTOCOL = "mariadb"; + + @Override + protected void buildProperties(MapMapper map, Map properties, Binding binding, String fnc) + { + + //jdbc properties + map.from(fnc + "-username").to(formatJDBCTemplate(fnc, "username")); + map.from(fnc + "-password").to(formatJDBCTemplate(fnc, "password")); + map.from(fnc + "-host", fnc + "-port", fnc + "-database").to(formatJDBCTemplate(fnc, "url"), + (host, port, database) -> String.format("jdbc:%s://%s:%s/%s", evalProtocol(), host, port, database)); + + // jdbcURL takes precedence + map.from(fnc + "-jdbc-url").to(formatJDBCTemplate(fnc, "url")); + + properties.put(formatJDBCTemplate(fnc, "driver-class-name"), "com.ibm.db2.jcc.DB2Driver"); + try { + Class.forName("org.mariadb.jdbc.Driver", false, getClass().getClassLoader()); + properties.put(formatJDBCTemplate(fnc, "driver-class-name"), "org.mariadb.jdbc.Driver"); + } catch (ClassNotFoundException e) { + try { + Class.forName("com.mysql.cj.jdbc.Driver", false, getClass().getClassLoader()); + properties.put(formatJDBCTemplate(fnc, "driver-class-name"), "com.mysql.cj.jdbc.Driver"); + } catch (ClassNotFoundException ignored) { + } + } + + + //r2dbc properties + map.from(fnc + "-username").to(formatR2DBCTemplate(fnc, "username")); + map.from(fnc + "-password").to(formatR2DBCTemplate(fnc, "password")); + map.from(fnc + "-host", fnc + "-port", fnc + "-database").to(formatR2DBCTemplate(fnc, "url"), + (host, port, database) -> String.format("r2dbc:%s://%s:%s/%s", evalProtocol(), host, port, database)); + + // r2dbcURL takes precedence + map.from(fnc + "-r2dbc-url").to(formatR2DBCTemplate(fnc, "url")); + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + public void onApplicationEvent(ApplicationPreparedEvent event) { + LOG.replayTo(getClass()); + } + + private String evalProtocol() + { + // Default to "mysql" + String connectionProtocol = MYSQL_PROTOCOL; + + /* Starting with Spring Boot 2.7.0, the previous MySQL r2dbc driver is no longer supported and + * documentation suggests using the MariaDB R2DBC driver as an alternative. Some versions + * of the MariaDB R2DBC driver do not support "mysql" as part of the connection + * protocol; "mariadb" should be used instead when the MariaDB R2DBC driver class is on + * the classpath. + */ + + try { + Class.forName("org.mariadb.r2dbc.MariadbConnection"); + connectionProtocol = MARIADB_PROTOCOL; + } + catch (ClassNotFoundException ignored) { + } + + return connectionProtocol; + } +} diff --git a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/OracleReplicatedBindingsPropertiesProcessor.java b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/OracleReplicatedBindingsPropertiesProcessor.java new file mode 100644 index 0000000..c823963 --- /dev/null +++ b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/OracleReplicatedBindingsPropertiesProcessor.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 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.Map; + +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.logging.DeferredLog; +import org.springframework.cloud.bindings.Binding; +import org.springframework.context.ApplicationListener; + +/** + * An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s of type: {@value TYPE}. + */ +public final class OracleReplicatedBindingsPropertiesProcessor extends AbstractReplicatedDataSource + implements ApplicationListener { + + /** + * The {@link Binding} type that this processor is interested in: {@value}. + **/ + public static final String TYPE = "oracle-replicated"; + + private static final DeferredLog LOG = new DeferredLog(); + + @Override + protected void buildProperties(MapMapper map, Map properties, Binding binding, String fnc) + { + + //jdbc properties + map.from(fnc + "-username").to(formatJDBCTemplate(fnc, "username")); + map.from(fnc + "-password").to(formatJDBCTemplate(fnc, "password")); + map.from(fnc + "-host", fnc + "-port", fnc + "-database").to(formatJDBCTemplate(fnc, "url"), + (host, port, database) -> String.format("jdbc:oracle://%s:%s/%s", host, port, database)); + + // jdbcURL takes precedence + map.from(fnc + "-jdbc-url").to(formatJDBCTemplate(fnc, "url")); + + properties.put(formatJDBCTemplate(fnc, "driver-class-name"), "oracle.jdbc.OracleDriver"); + + //r2dbc properties + map.from(fnc + "-username").to(formatR2DBCTemplate(fnc, "username")); + map.from(fnc + "-password").to(formatR2DBCTemplate(fnc, "password")); + map.from(fnc + "-host", fnc + "-port", fnc + "-database").to(formatR2DBCTemplate(fnc, "url"), + (host, port, database) -> String.format("r2dbc:oracle://%s:%s/%s", host, port, database)); + + // r2dbcURL takes precedence + map.from(fnc + "-r2dbc-url").to(formatR2DBCTemplate(fnc, "url")); + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + public void onApplicationEvent(ApplicationPreparedEvent event) { + LOG.replayTo(getClass()); + } + +} diff --git a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessor.java b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessor.java index 4cdd783..6a5b8f5 100644 --- a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessor.java +++ b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessor.java @@ -20,9 +20,6 @@ import org.springframework.cloud.bindings.Bindings; import org.springframework.core.env.Environment; -import java.nio.file.FileSystems; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import static org.springframework.cloud.bindings.boot.Guards.isTypeEnabled; @@ -32,25 +29,13 @@ * * @see JDBC URL Format */ -public final class PostgreSqlBindingsPropertiesProcessor implements BindingsPropertiesProcessor { +public final class PostgreSqlBindingsPropertiesProcessor extends AbstractPostgreSQLBindingsPropertiesProcessor { /** * The {@link Binding} type that this processor is interested in: {@value}. **/ public static final String TYPE = "postgresql"; - /** - * sslmode determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. - */ - public static final String SSL_MODE = "sslmode"; - /** - * sslrootcert specifies the name of a file containing SSL certificate authority (CA) certificate(s). - */ - public static final String SSL_ROOT_CERT = "sslrootcert"; - /** - * options Specifies command-line options to send to the server at connection start. - * CockroachDB uses this to pass in cluster routing id - */ - public static final String OPTIONS = "options"; + public static final String SPRING_DATASOURCE_URL = "spring.datasource.url"; public static final String SPRING_R2DBC_URL = "spring.r2dbc.url"; @@ -99,69 +84,4 @@ public void process(Environment environment, Bindings bindings, MapPostgreSQL Doc. - *

- * CockroachDB, which shares the same 'postgresql://' protocol as PostgreSQL, has customized options to meet its - * distributed database nature. - * Refer to Client Connection Parameters. - */ - private String buildDbOptions(Binding binding) { - String options = binding.getSecret().getOrDefault(OPTIONS, ""); - String crdbOption = ""; - List dbOptions = new ArrayList<>(); - if (!options.equals("")) { - String[] allOpts = options.split("&"); - for (String o : allOpts) { - String[] keyval = o.split("="); - if (keyval.length != 2 || keyval[0].length() == 0 || keyval[1].length() == 0) { - continue; - } - if (keyval[0].equals("--cluster")) { - crdbOption = keyval[0] + "=" + keyval[1]; - } else { - dbOptions.add("-c " + keyval[0] + "=" + keyval[1]); - } - } - } - String combinedOptions = crdbOption; - if (dbOptions.size() > 0) { - String otherOpts = String.join(" ", dbOptions); - if (!combinedOptions.equals("")) { - combinedOptions = combinedOptions + " " + otherOpts; - } else { - combinedOptions = otherOpts; - } - } - if (!"".equals(combinedOptions)) { - combinedOptions = "options=" + combinedOptions; - } - return combinedOptions; - } - - /** - * Returns a concatenated string of all ssl parameters for enabling one-way TLS (PostgreSQL certifies itself) - * Refer to PostgreSQL Doc - */ - private String buildSslModeParam(Binding binding) { - //process ssl params - //https://www.postgresql.org/docs/14/libpq-connect.html - String sslmode = binding.getSecret().getOrDefault(SSL_MODE, ""); - String sslRootCert = binding.getSecret().getOrDefault(SSL_ROOT_CERT, ""); - StringBuilder sslparam = new StringBuilder(); - if (!"".equals(sslmode)) { - sslparam.append(SSL_MODE).append("=").append(sslmode); - } - if (!"".equals(sslRootCert)) { - if (!"".equals(sslmode)) { - sslparam.append("&"); - } - sslparam.append(SSL_ROOT_CERT).append("=") - .append(binding.getPath()).append(FileSystems.getDefault().getSeparator()) - .append(sslRootCert); - } - return sslparam.toString(); - } } diff --git a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlReplicatedBindingsPropertiesProcessor.java b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlReplicatedBindingsPropertiesProcessor.java new file mode 100644 index 0000000..fbf3b2e --- /dev/null +++ b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlReplicatedBindingsPropertiesProcessor.java @@ -0,0 +1,171 @@ +/* + * Copyright 2024 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.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.logging.DeferredLog; +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.context.ApplicationListener; +import org.springframework.core.env.Environment; + +import java.util.Map; + +import static org.springframework.cloud.bindings.boot.Guards.isTypeEnabled; + +/** + * An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s of type: {@value TYPE}. + */ +public final class PostgreSqlReplicatedBindingsPropertiesProcessor extends AbstractPostgreSQLBindingsPropertiesProcessor + implements BindingsPropertiesProcessor, ApplicationListener { + + /** + * The {@link Binding} type that this processor is interested in: {@value}. + **/ + public static final String TYPE = "postgresql-replicated"; + + private static final DeferredLog LOG = new DeferredLog(); + + /** + * Read/write function field + */ + private static final String RW_FUNCTION = "rw"; + + /** + * Read only function field + */ + private static final String RO_FUNCTION = "ro"; + + /** + * Template for a base JDBC properties + */ + private static final String JDBC_BASE_TEMPLATE = "spring.datasource.replicated.%s"; + + /** + * Template for a base R2DBC properties + */ + private static final String R2DBC_BASE_TEMPLATE = "spring.r2dbc.replicated.%s"; + + /** + * Template for function based JDBC properties + */ + private static final String JDBC_PROPERTY_TEMPLATE = "spring.datasource.replicated.%s.%s"; + + /** + * Template for function based R2DBC properties + */ + private static final String R2DBC_PROPERTY_TEMPLATE = "spring.r2dbc.replicated.%s.%s"; + + private String mode = RW_FUNCTION; + + @Override + public void process(Environment environment, Bindings bindings, Map properties) { + + if (!isTypeEnabled(environment, TYPE)) { + return; + } + + bindings.filterBindings(TYPE).forEach(binding -> { + + properties.put(String.format(JDBC_BASE_TEMPLATE, "name"), binding.getName()); + + properties.put(String.format(R2DBC_BASE_TEMPLATE, "name"), binding.getName()); + + final var map = new MapMapper(binding.getSecret(), properties); + + mode = RW_FUNCTION; + buildProperties(map, properties, binding, RW_FUNCTION); + + mode = RO_FUNCTION; + buildProperties(map, properties, binding, RO_FUNCTION); + + }); + } + + private void buildProperties(MapMapper map, Map properties, Binding binding, String fnc) + { + map.from(fnc + "-password").to(formatJDBCTemplate(fnc, "password")); + + map.from(fnc + "-host", fnc + "-port", fnc + "-database").to(formatJDBCTemplate(fnc, "url"), + (host, port, database) -> String.format("jdbc:postgresql://%s:%s/%s", host, port, database)); + + String sslParam = buildSslModeParam(binding); + String sslModeOptions = buildDbOptions(binding); + if (!"".equals(sslParam) && !"".equals(sslModeOptions)) { + sslModeOptions = sslParam + "&" + sslModeOptions; + } else if (!"".equals(sslParam) ) { + sslModeOptions = sslParam; + } + + if (!"".equals(sslModeOptions)) { + properties.put(formatJDBCTemplate(fnc, "url") , + properties.get(formatJDBCTemplate(fnc, "url")) + "?" + sslModeOptions); + } + map.from(fnc + "-username").to(formatJDBCTemplate(fnc, "username")); + + // jdbcURL takes precedence + map.from(fnc + "-jdbc-url").to(formatJDBCTemplate(fnc, "url")); + + properties.put(formatJDBCTemplate(fnc, "driver-class-name"), "org.postgresql.Driver"); + + //r2dbc properties + map.from(fnc + "-password").to(formatR2DBCTemplate(fnc, "password")); + map.from(fnc + "-host", fnc + "-port", fnc + "-database").to(formatR2DBCTemplate(fnc, "url"), + (host, port, database) -> String.format("r2dbc:postgresql://%s:%s/%s", host, port, database)); + if (!"".equals(sslModeOptions)) { + properties.put(formatR2DBCTemplate(fnc, "url") , + properties.get(formatR2DBCTemplate(fnc, "url")) + "?" + sslModeOptions); + } + map.from(fnc + "-username").to(formatR2DBCTemplate(fnc, "username")); + + // r2dbcURL takes precedence + map.from(fnc + "-r2dbc-url").to(formatR2DBCTemplate(fnc, "url")); + } + + private String formatJDBCTemplate(String fnc, String field) + { + return String.format(JDBC_PROPERTY_TEMPLATE, fnc, field); + } + + private String formatR2DBCTemplate(String fnc, String field) + { + return String.format(R2DBC_PROPERTY_TEMPLATE, fnc, field); + } + + @Override + protected String getDBOptionSecretField() + { + return String.format("%s-%s", mode , OPTIONS); + } + + @Override + protected String getSSLModeSecretField() + { + return String.format("%s-%s", mode , SSL_MODE); + } + + @Override + protected String getSSLRootCertSecretField() + { + return String.format("%s-%s", mode , SSL_ROOT_CERT); + } + + @Override + public void onApplicationEvent(ApplicationPreparedEvent event) { + LOG.replayTo(getClass()); + } +} diff --git a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/SapHanaReplicatedBindingsPropertiesProcessor.java b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/SapHanaReplicatedBindingsPropertiesProcessor.java new file mode 100644 index 0000000..b2bce25 --- /dev/null +++ b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/SapHanaReplicatedBindingsPropertiesProcessor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024 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.Map; + +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.logging.DeferredLog; +import org.springframework.cloud.bindings.Binding; +import org.springframework.context.ApplicationListener; + +/** + * An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s of type: {@value TYPE}. + */ +public final class SapHanaReplicatedBindingsPropertiesProcessor extends AbstractReplicatedDataSource + implements ApplicationListener { + + /** + * The {@link Binding} type that this processor is interested in: {@value}. + **/ + public static final String TYPE = "hana-replicated"; + + private static final DeferredLog LOG = new DeferredLog(); + + @Override + protected void buildProperties(MapMapper map, Map properties, Binding binding, String fnc) + { + //jdbc properties + map.from(fnc + "-username").to(formatJDBCTemplate(fnc, "username")); + map.from(fnc + "-password").to(formatJDBCTemplate(fnc, "password")); + map.from(fnc + "-host", fnc + "-port", fnc + "-database").to(formatJDBCTemplate(fnc, "url"), + (host, port, database) -> String.format("jdbc:sap://%s:%s/%s", host, port, database)); + + // jdbcURL takes precedence + map.from(fnc + "-jdbc-url").to(formatJDBCTemplate(fnc, "url")); + + properties.put(formatJDBCTemplate(fnc, "driver-class-name"), "com.sap.db.jdbc.Driver"); + + //r2dbc properties + map.from(fnc + "-username").to(formatR2DBCTemplate(fnc, "username")); + map.from(fnc + "-password").to(formatR2DBCTemplate(fnc, "password")); + map.from(fnc + "-host", fnc + "-port", fnc + "-database").to(formatR2DBCTemplate(fnc, "url"), + (host, port, database) -> String.format("r2dbc:sap://%s:%s/%s", host, port, database)); + + // r2dbcURL takes precedence + map.from(fnc + "-r2dbc-url").to(formatR2DBCTemplate(fnc, "url")); + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + public void onApplicationEvent(ApplicationPreparedEvent event) { + LOG.replayTo(getClass()); + } +} diff --git a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/SqlServerReplicatedBindingsPropertiesProcessor.java b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/SqlServerReplicatedBindingsPropertiesProcessor.java new file mode 100644 index 0000000..d096bf8 --- /dev/null +++ b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/SqlServerReplicatedBindingsPropertiesProcessor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024 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.Map; + +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.logging.DeferredLog; +import org.springframework.cloud.bindings.Binding; +import org.springframework.context.ApplicationListener; + +/** + * An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s of type: {@value TYPE}. + */ +public final class SqlServerReplicatedBindingsPropertiesProcessor extends AbstractReplicatedDataSource + implements ApplicationListener { + + /** + * The {@link Binding} type that this processor is interested in: {@value}. + **/ + public static final String TYPE = "sqlserver-replicated"; + + private static final DeferredLog LOG = new DeferredLog(); + + @Override + protected void buildProperties(MapMapper map, Map properties, Binding binding, String fnc) + { + //jdbc properties + map.from(fnc + "-username").to(formatJDBCTemplate(fnc, "username")); + map.from(fnc + "-password").to(formatJDBCTemplate(fnc, "password")); + map.from(fnc + "-host", fnc + "-port", fnc + "-database").to(formatJDBCTemplate(fnc, "url"), + (host, port, database) -> String.format("jdbc:sqlserver://%s:%s/%s", host, port, database)); + + // jdbcURL takes precedence + map.from(fnc + "-jdbc-url").to(formatJDBCTemplate(fnc, "url")); + + properties.put(formatJDBCTemplate(fnc, "driver-class-name"), "com.microsoft.sqlserver.jdbc.SQLServerDriver"); + + //r2dbc properties + map.from(fnc + "-username").to(formatR2DBCTemplate(fnc, "username")); + map.from(fnc + "-password").to(formatR2DBCTemplate(fnc, "password")); + map.from(fnc + "-host", fnc + "-port", fnc + "-database").to(formatR2DBCTemplate(fnc, "url"), + (host, port, database) -> String.format("r2dbc:sqlserver://%s:%s/%s", host, port, database)); + + // r2dbcURL takes precedence + map.from(fnc + "-r2dbc-url").to(formatR2DBCTemplate(fnc, "url")); + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + public void onApplicationEvent(ApplicationPreparedEvent event) { + LOG.replayTo(getClass()); + } +} diff --git a/spring-cloud-bindings/src/main/resources/META-INF/spring.factories b/spring-cloud-bindings/src/main/resources/META-INF/spring.factories index cde7fc3..1f60ac9 100644 --- a/spring-cloud-bindings/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-bindings/src/main/resources/META-INF/spring.factories @@ -1,7 +1,13 @@ org.springframework.context.ApplicationListener=\ org.springframework.cloud.bindings.boot.BindingFlattenedEnvironmentPostProcessor, \ org.springframework.cloud.bindings.boot.BindingSpecificEnvironmentPostProcessor, \ + org.springframework.cloud.bindings.boot.Db2ReplicatedBindingsPropertiesProcessor, \ + org.springframework.cloud.bindings.boot.MySqlReplicatedBindingsPropertiesProcessor, \ + org.springframework.cloud.bindings.boot.OracleReplicatedBindingsPropertiesProcessor, \ + org.springframework.cloud.bindings.boot.PostgreSqlReplicatedBindingsPropertiesProcessor, \ + org.springframework.cloud.bindings.boot.SapHanaReplicatedBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.SpringSecurityOAuth2BindingsPropertiesProcessor, \ + org.springframework.cloud.bindings.boot.SqlServerReplicatedBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.VaultBindingsPropertiesProcessor org.springframework.boot.env.EnvironmentPostProcessor=\ org.springframework.cloud.bindings.boot.BindingFlattenedEnvironmentPostProcessor, \ @@ -13,19 +19,25 @@ org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor=\ org.springframework.cloud.bindings.boot.ConfigServerBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.CouchbaseBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.Db2BindingsPropertiesProcessor, \ + org.springframework.cloud.bindings.boot.Db2ReplicatedBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.ElasticsearchBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.EurekaBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.KafkaBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.LDAPBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.MongoDbBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.MySqlBindingsPropertiesProcessor, \ + org.springframework.cloud.bindings.boot.MySqlReplicatedBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.Neo4JBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.OracleBindingsPropertiesProcessor, \ + org.springframework.cloud.bindings.boot.OracleReplicatedBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.PostgreSqlBindingsPropertiesProcessor, \ + org.springframework.cloud.bindings.boot.PostgreSqlReplicatedBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.RabbitMqBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.RedisBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.SapHanaBindingsPropertiesProcessor, \ + org.springframework.cloud.bindings.boot.SapHanaReplicatedBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.SpringSecurityOAuth2BindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.SqlServerBindingsPropertiesProcessor, \ + org.springframework.cloud.bindings.boot.SqlServerReplicatedBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.VaultBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.WavefrontBindingsPropertiesProcessor diff --git a/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/BindingSpecificEnvironmentPostProcessorTest.java b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/BindingSpecificEnvironmentPostProcessorTest.java index 85a7d7b..4646db8 100644 --- a/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/BindingSpecificEnvironmentPostProcessorTest.java +++ b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/BindingSpecificEnvironmentPostProcessorTest.java @@ -104,7 +104,7 @@ void order() { @Test @DisplayName("included implementations are registered") void includedImplementations() { - assertThat(new BindingSpecificEnvironmentPostProcessor().processors).hasSize(21); + assertThat(new BindingSpecificEnvironmentPostProcessor().processors).hasSize(27); } } diff --git a/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/Db2ReplicatedBindingsPropertiesProcessorTest.java b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/Db2ReplicatedBindingsPropertiesProcessorTest.java new file mode 100644 index 0000000..e830ac7 --- /dev/null +++ b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/Db2ReplicatedBindingsPropertiesProcessorTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2024 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 static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.bindings.boot.Db2ReplicatedBindingsPropertiesProcessor.TYPE; + +import java.nio.file.Paths; +import java.util.HashMap; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.cloud.bindings.FluentMap; +import org.springframework.mock.env.MockEnvironment; + +@DisplayName("DB2Replicated BindingsPropertiesProcessor") +public class Db2ReplicatedBindingsPropertiesProcessorTest { + + private final FluentMap secret = new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("rw-database", "testrw-database") + .withEntry("rw-host", "testrw-host") + .withEntry("rw-password", "testrw-password") + .withEntry("rw-port", "testrw-port") + .withEntry("rw-username", "testrw-username") + .withEntry("ro-database", "testro-database") + .withEntry("ro-host", "testro-host") + .withEntry("ro-password", "testro-password") + .withEntry("ro-port", "testro-port") + .withEntry("ro-username", "testro-username"); + + private final MockEnvironment environment = new MockEnvironment(); + + private final HashMap properties = new HashMap<>(); + + @Test + @DisplayName("composes jdbc url from host port and database") + void testJdbc() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + new Db2ReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "com.ibm.db2.jcc.DB2Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "jdbc:db2://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.datasource.replicated.ro.driver-class-name", "com.ibm.db2.jcc.DB2Driver") + .containsEntry("spring.datasource.replicated.ro.password", "testro-password") + .containsEntry("spring.datasource.replicated.ro.url", "jdbc:db2://testro-host:testro-port/testro-database") + .containsEntry("spring.datasource.replicated.ro.username", "testro-username") + .containsEntry("spring.datasource.replicated.name", "dbrw"); + } + + @Test + @DisplayName("gives precedence to jdbc-url") + void testJdbcURL() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-jdbc-url", "testrw-jdbc-url").withEntry("ro-jdbc-url", "testro-jdbc-url")) + ); + new Db2ReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "com.ibm.db2.jcc.DB2Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "testrw-jdbc-url") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.datasource.replicated.ro.driver-class-name", "com.ibm.db2.jcc.DB2Driver") + .containsEntry("spring.datasource.replicated.ro.password", "testro-password") + .containsEntry("spring.datasource.replicated.ro.url", "testro-jdbc-url") + .containsEntry("spring.datasource.replicated.ro.username", "testro-username") + .containsEntry("spring.datasource.replicated.name", "dbrw"); + } + + @Test + @DisplayName("contributes r2dbc properties") + void testR2dbc() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + new Db2ReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "r2dbc:db2://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.ro.password", "testro-password") + .containsEntry("spring.r2dbc.replicated.ro.url", "r2dbc:db2://testro-host:testro-port/testro-database") + .containsEntry("spring.r2dbc.replicated.ro.username", "testro-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("gives precedence to r2dbc-url") + void testR2dbcURL() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-r2dbc-url", "testrw-r2dbc-url").withEntry("ro-r2dbc-url", "testro-r2dbc-url")) + ); + new Db2ReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "testrw-r2dbc-url") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.ro.password", "testro-password") + .containsEntry("spring.r2dbc.replicated.ro.url", "testro-r2dbc-url") + .containsEntry("spring.r2dbc.replicated.ro.username", "testro-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("can be disabled") + void disabled() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + environment.setProperty("org.springframework.cloud.bindings.boot.db2-replicated.enable", "false"); + + new Db2ReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + + assertThat(properties).isEmpty(); + } +} diff --git a/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/MySqlReplicatedBindingsPropertiesProcessorTest.java b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/MySqlReplicatedBindingsPropertiesProcessorTest.java new file mode 100644 index 0000000..8573231 --- /dev/null +++ b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/MySqlReplicatedBindingsPropertiesProcessorTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2024 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 static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.bindings.boot.MySqlReplicatedBindingsPropertiesProcessor.TYPE; + +import java.nio.file.Paths; +import java.util.HashMap; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.cloud.bindings.FluentMap; +import org.springframework.mock.env.MockEnvironment; + +@DisplayName("MySQLReplicated BindingsPropertiesProcessor") +public class MySqlReplicatedBindingsPropertiesProcessorTest { + + private final FluentMap secret = new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("rw-database", "testrw-database") + .withEntry("rw-host", "testrw-host") + .withEntry("rw-password", "testrw-password") + .withEntry("rw-port", "testrw-port") + .withEntry("rw-username", "testrw-username") + .withEntry("ro-database", "testro-database") + .withEntry("ro-host", "testro-host") + .withEntry("ro-password", "testro-password") + .withEntry("ro-port", "testro-port") + .withEntry("ro-username", "testro-username"); + + private final MockEnvironment environment = new MockEnvironment(); + + private final HashMap properties = new HashMap<>(); + + @Test + @DisplayName("composes jdbc url from host port and database") + void testJdbc() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + new MySqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "org.mariadb.jdbc.Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "jdbc:mariadb://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.datasource.replicated.ro.driver-class-name", "org.mariadb.jdbc.Driver") + .containsEntry("spring.datasource.replicated.ro.password", "testro-password") + .containsEntry("spring.datasource.replicated.ro.url", "jdbc:mariadb://testro-host:testro-port/testro-database") + .containsEntry("spring.datasource.replicated.ro.username", "testro-username") + .containsEntry("spring.datasource.replicated.name", "dbrw"); + } + + @Test + @DisplayName("gives precedence to jdbc-url") + void testJdbcURL() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-jdbc-url", "testrw-jdbc-url").withEntry("ro-jdbc-url", "testro-jdbc-url")) + ); + new MySqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "org.mariadb.jdbc.Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "testrw-jdbc-url") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.datasource.replicated.ro.driver-class-name", "org.mariadb.jdbc.Driver") + .containsEntry("spring.datasource.replicated.ro.password", "testro-password") + .containsEntry("spring.datasource.replicated.ro.url", "testro-jdbc-url") + .containsEntry("spring.datasource.replicated.ro.username", "testro-username") + .containsEntry("spring.datasource.replicated.name", "dbrw"); + } + + @Test + @DisplayName("composes r2dbc url from host port and database") + void testR2dbc() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + new MySqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "r2dbc:mariadb://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.ro.password", "testro-password") + .containsEntry("spring.r2dbc.replicated.ro.url", "r2dbc:mariadb://testro-host:testro-port/testro-database") + .containsEntry("spring.r2dbc.replicated.ro.username", "testro-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("gives precedence to r2dbc-url") + void testR2dbcURL() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-r2dbc-url", "testrw-r2dbc-url").withEntry("ro-r2dbc-url", "testro-r2dbc-url")) + ); + new MySqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "testrw-r2dbc-url") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.ro.password", "testro-password") + .containsEntry("spring.r2dbc.replicated.ro.url", "testro-r2dbc-url") + .containsEntry("spring.r2dbc.replicated.ro.username", "testro-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("can be disabled") + void disabled() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + environment.setProperty("org.springframework.cloud.bindings.boot.mysql-replicated.enable", "false"); + + new MySqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + + assertThat(properties).isEmpty(); + } +} diff --git a/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/OracleReplicatedBindingsPropertiesProcessorTest.java b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/OracleReplicatedBindingsPropertiesProcessorTest.java new file mode 100644 index 0000000..ec9a3dc --- /dev/null +++ b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/OracleReplicatedBindingsPropertiesProcessorTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2024 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 static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.bindings.boot.OracleReplicatedBindingsPropertiesProcessor.TYPE; + +import java.nio.file.Paths; +import java.util.HashMap; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.cloud.bindings.FluentMap; +import org.springframework.mock.env.MockEnvironment; + +@DisplayName("OracleReplicated BindingsPropertiesProcessor") +public class OracleReplicatedBindingsPropertiesProcessorTest { + private final FluentMap secret = new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("rw-database", "testrw-database") + .withEntry("rw-host", "testrw-host") + .withEntry("rw-password", "testrw-password") + .withEntry("rw-port", "testrw-port") + .withEntry("rw-username", "testrw-username") + .withEntry("ro-database", "testro-database") + .withEntry("ro-host", "testro-host") + .withEntry("ro-password", "testro-password") + .withEntry("ro-port", "testro-port") + .withEntry("ro-username", "testro-username"); + + private final MockEnvironment environment = new MockEnvironment(); + + private final HashMap properties = new HashMap<>(); + + @Test + @DisplayName("composes jdbc url from host port and database") + void testJdbc() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + new OracleReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "oracle.jdbc.OracleDriver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "jdbc:oracle://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.datasource.replicated.ro.driver-class-name", "oracle.jdbc.OracleDriver") + .containsEntry("spring.datasource.replicated.ro.password", "testro-password") + .containsEntry("spring.datasource.replicated.ro.url", "jdbc:oracle://testro-host:testro-port/testro-database") + .containsEntry("spring.datasource.replicated.ro.username", "testro-username") + .containsEntry("spring.datasource.replicated.name", "dbrw"); + } + + @Test + @DisplayName("gives precedence to jdbc-url") + void testJdbcURL() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-jdbc-url", "testrw-jdbc-url").withEntry("ro-jdbc-url", "testro-jdbc-url")) + ); + new OracleReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "oracle.jdbc.OracleDriver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "testrw-jdbc-url") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.datasource.replicated.ro.driver-class-name", "oracle.jdbc.OracleDriver") + .containsEntry("spring.datasource.replicated.ro.password", "testro-password") + .containsEntry("spring.datasource.replicated.ro.url", "testro-jdbc-url") + .containsEntry("spring.datasource.replicated.ro.username", "testro-username") + .containsEntry("spring.datasource.replicated.name", "dbrw"); + } + + @Test + @DisplayName("composes r2dbc url from host port and database") + void testR2dbc() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + new OracleReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "r2dbc:oracle://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.ro.password", "testro-password") + .containsEntry("spring.r2dbc.replicated.ro.url", "r2dbc:oracle://testro-host:testro-port/testro-database") + .containsEntry("spring.r2dbc.replicated.ro.username", "testro-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("gives precedence to r2dbc-url") + void testR2dbcURL() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-r2dbc-url", "testrw-r2dbc-url").withEntry("ro-r2dbc-url", "testro-r2dbc-url")) + ); + new OracleReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "testrw-r2dbc-url") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.ro.password", "testro-password") + .containsEntry("spring.r2dbc.replicated.ro.url", "testro-r2dbc-url") + .containsEntry("spring.r2dbc.replicated.ro.username", "testro-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("can be disabled") + void disabled() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + environment.setProperty("org.springframework.cloud.bindings.boot.oracle-replicated.enable", "false"); + + new OracleReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + + assertThat(properties).isEmpty(); + } +} diff --git a/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/PostgreSqlReplicatedBindingsPropertiesProcessorTest.java b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/PostgreSqlReplicatedBindingsPropertiesProcessorTest.java new file mode 100644 index 0000000..af71f84 --- /dev/null +++ b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/PostgreSqlReplicatedBindingsPropertiesProcessorTest.java @@ -0,0 +1,250 @@ +/* + * Copyright 2024 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 static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.bindings.boot.PostgreSqlReplicatedBindingsPropertiesProcessor.TYPE; + +import java.nio.file.Paths; +import java.util.HashMap; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.cloud.bindings.FluentMap; +import org.springframework.mock.env.MockEnvironment; + +@DisplayName("PostgreSQLReplicated BindingsPropertiesProcessor") +public class PostgreSqlReplicatedBindingsPropertiesProcessorTest { + + private final FluentMap secret = new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("rw-database", "testrw-database") + .withEntry("rw-host", "testrw-host") + .withEntry("rw-password", "testrw-password") + .withEntry("rw-port", "testrw-port") + .withEntry("rw-username", "testrw-username") + .withEntry("ro-database", "testro-database") + .withEntry("ro-host", "testro-host") + .withEntry("ro-password", "testro-password") + .withEntry("ro-port", "testro-port") + .withEntry("ro-username", "testro-username"); + + private final MockEnvironment environment = new MockEnvironment(); + + private final HashMap properties = new HashMap<>(); + + @Test + @DisplayName("composes jdbc url from host port and database") + void testJdbc() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + new PostgreSqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "jdbc:postgresql://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.datasource.replicated.ro.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.replicated.ro.password", "testro-password") + .containsEntry("spring.datasource.replicated.ro.url", "jdbc:postgresql://testro-host:testro-port/testro-database") + .containsEntry("spring.datasource.replicated.ro.username", "testro-username") + .containsEntry("spring.datasource.replicated.name", "dbrw"); + + } + + + @Test + @DisplayName("gives precedence to jdbc-url") + void testJdbcURL() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-jdbc-url", "testrw-jdbc-url").withEntry("ro-jdbc-url", "testro-jdbc-url")) + ); + new PostgreSqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "testrw-jdbc-url") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.datasource.replicated.ro.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.replicated.ro.password", "testro-password") + .containsEntry("spring.datasource.replicated.ro.url", "testro-jdbc-url") + .containsEntry("spring.datasource.replicated.ro.username", "testro-username") + .containsEntry("spring.datasource.replicated.name", "dbrw"); + } + + @Test + @DisplayName("composes r2dbc url from host port and database") + void testR2dbc() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + new PostgreSqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "r2dbc:postgresql://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.ro.password", "testro-password") + .containsEntry("spring.r2dbc.replicated.ro.url", "r2dbc:postgresql://testro-host:testro-port/testro-database") + .containsEntry("spring.r2dbc.replicated.ro.username", "testro-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("gives precedence to r2dbc-url") + void testR2dbcURL() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-r2dbc-url", "testrw-r2dbc-url").withEntry("ro-r2dbc-url", "testro-r2dbc-url")) + ); + new PostgreSqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "testrw-r2dbc-url") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.ro.password", "testro-password") + .containsEntry("spring.r2dbc.replicated.ro.url", "testro-r2dbc-url") + .containsEntry("spring.r2dbc.replicated.ro.username", "testro-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + + @Test + @DisplayName("can be disabled") + void disabled() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + environment.setProperty("org.springframework.cloud.bindings.boot.postgresql-replicated.enable", "false"); + + new PostgreSqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + + assertThat(properties).isEmpty(); + } + + @Test + @DisplayName("composes jdbc url from host port and database with sslmode and crdb option") + void testJdbcWithSsl() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-sslmode", "verify-full") + .withEntry("rw-sslrootcert", "root.cert") + .withEntry("rw-options", "--cluster=routing-id&opt=val1")) + ); + new PostgreSqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "jdbc:postgresql://testrw-host:testrw-port/testrw-database?sslmode=verify-full&sslrootcert=dbrw-path/root.cert&options=--cluster=routing-id -c opt=val1") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("composes jdbc url from host port and database with sslmode and crdb option") + void testJdbcWithInvalidCrdbOption() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-sslmode", "verify-full") + .withEntry("rw-sslrootcert", "root.cert") + .withEntry("rw-options", "-cluster=routing-id&opt=val1")) + ); + new PostgreSqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "jdbc:postgresql://testrw-host:testrw-port/testrw-database?sslmode=verify-full&sslrootcert=dbrw-path/root.cert&options=-c -cluster=routing-id -c opt=val1") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("composes r2dbc url from host port and database with sslmode and crdb option") + void testR2dbcWithSsl() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-sslmode", "verify-full") + .withEntry("rw-sslrootcert", "root.cert") + .withEntry("rw-options", "--cluster=routing-id&opt=val1")) + ); + new PostgreSqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "r2dbc:postgresql://testrw-host:testrw-port/testrw-database?sslmode=verify-full&sslrootcert=dbrw-path/root.cert&options=--cluster=routing-id -c opt=val1") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("composes jdbc url from host port and database with sslmode disable") + void testJdbcWithSslDisable() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-sslmode", "disable")) + ); + new PostgreSqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "jdbc:postgresql://testrw-host:testrw-port/testrw-database?sslmode=disable") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("composes jdbc url from host port and database with DB options") + void testJdbcWithDBoptions() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-options", "opt1=val1&opt2=val2")) + ); + new PostgreSqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "jdbc:postgresql://testrw-host:testrw-port/testrw-database?options=-c opt1=val1 -c opt2=val2") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("composes jdbc url from host port and database with invalid DB options") + void testJdbcWithInvaildDBoptions() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-options", "opt1=val1&opt")) + ); + new PostgreSqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "jdbc:postgresql://testrw-host:testrw-port/testrw-database?options=-c opt1=val1") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("composes jdbc url from host port and database with empty DB options") + void testJdbcWithEmptyDBoptions() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-options", "")) + ); + new PostgreSqlReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "jdbc:postgresql://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + +} diff --git a/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/SapHanaReplicatedBindingsPropertiesProcessorTest.java b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/SapHanaReplicatedBindingsPropertiesProcessorTest.java new file mode 100644 index 0000000..1c6e2ce --- /dev/null +++ b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/SapHanaReplicatedBindingsPropertiesProcessorTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2024 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 static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.bindings.boot.SapHanaReplicatedBindingsPropertiesProcessor.TYPE; + +import java.nio.file.Paths; +import java.util.HashMap; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.cloud.bindings.FluentMap; +import org.springframework.mock.env.MockEnvironment; + +@DisplayName("SAP HanaRepicated BindingsPropertiesProcessor") +public class SapHanaReplicatedBindingsPropertiesProcessorTest { + + private final FluentMap secret = new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("rw-database", "testrw-database") + .withEntry("rw-host", "testrw-host") + .withEntry("rw-password", "testrw-password") + .withEntry("rw-port", "testrw-port") + .withEntry("rw-username", "testrw-username") + .withEntry("ro-database", "testro-database") + .withEntry("ro-host", "testro-host") + .withEntry("ro-password", "testro-password") + .withEntry("ro-port", "testro-port") + .withEntry("ro-username", "testro-username"); + + private final MockEnvironment environment = new MockEnvironment(); + + private final HashMap properties = new HashMap<>(); + + @Test + @DisplayName("composes jdbc url from host port and database") + void testJdbc() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + new SapHanaReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "com.sap.db.jdbc.Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "jdbc:sap://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.datasource.replicated.ro.driver-class-name", "com.sap.db.jdbc.Driver") + .containsEntry("spring.datasource.replicated.ro.password", "testro-password") + .containsEntry("spring.datasource.replicated.ro.url", "jdbc:sap://testro-host:testro-port/testro-database") + .containsEntry("spring.datasource.replicated.ro.username", "testro-username") + .containsEntry("spring.datasource.replicated.name", "dbrw"); + } + + @Test + @DisplayName("gives precedence to jdbc-url") + void testJdbcURL() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-jdbc-url", "testrw-jdbc-url").withEntry("ro-jdbc-url", "testro-jdbc-url")) + ); + new SapHanaReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "com.sap.db.jdbc.Driver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "testrw-jdbc-url") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.datasource.replicated.ro.driver-class-name", "com.sap.db.jdbc.Driver") + .containsEntry("spring.datasource.replicated.ro.password", "testro-password") + .containsEntry("spring.datasource.replicated.ro.url", "testro-jdbc-url") + .containsEntry("spring.datasource.replicated.ro.username", "testro-username") + .containsEntry("spring.datasource.replicated.name", "dbrw"); + } + + @Test + @DisplayName("composes r2dbc url from host port and database") + void testR2dbc() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + new SapHanaReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "r2dbc:sap://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.ro.password", "testro-password") + .containsEntry("spring.r2dbc.replicated.ro.url", "r2dbc:sap://testro-host:testro-port/testro-database") + .containsEntry("spring.r2dbc.replicated.ro.username", "testro-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("gives precedence to r2dbc-url") + void testR2dbcURL() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-r2dbc-url", "testrw-r2dbc-url").withEntry("ro-r2dbc-url", "testro-r2dbc-url")) + ); + new SapHanaReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "testrw-r2dbc-url") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.ro.password", "testro-password") + .containsEntry("spring.r2dbc.replicated.ro.url", "testro-r2dbc-url") + .containsEntry("spring.r2dbc.replicated.ro.username", "testro-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("can be disabled") + void disabled() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + environment.setProperty("org.springframework.cloud.bindings.boot.hana-replicated.enable", "false"); + + new SapHanaReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + + assertThat(properties).isEmpty(); + } +} diff --git a/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/SqlServerReplicatedBindingsPropertiesProcessorTest.java b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/SqlServerReplicatedBindingsPropertiesProcessorTest.java new file mode 100644 index 0000000..287a65c --- /dev/null +++ b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/SqlServerReplicatedBindingsPropertiesProcessorTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2024 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 static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.bindings.boot.SqlServerReplicatedBindingsPropertiesProcessor.TYPE; + +import java.nio.file.Paths; +import java.util.HashMap; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.cloud.bindings.FluentMap; +import org.springframework.mock.env.MockEnvironment; + +@DisplayName("SQLServerReplicated BindingsPropertiesProcessor") +public class SqlServerReplicatedBindingsPropertiesProcessorTest { + + private final FluentMap secret = new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("rw-database", "testrw-database") + .withEntry("rw-host", "testrw-host") + .withEntry("rw-password", "testrw-password") + .withEntry("rw-port", "testrw-port") + .withEntry("rw-username", "testrw-username") + .withEntry("ro-database", "testro-database") + .withEntry("ro-host", "testro-host") + .withEntry("ro-password", "testro-password") + .withEntry("ro-port", "testro-port") + .withEntry("ro-username", "testro-username"); + + private final MockEnvironment environment = new MockEnvironment(); + + private final HashMap properties = new HashMap<>(); + + @Test + @DisplayName("composes jdbc url from host port and database") + void testJdbc() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + new SqlServerReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "com.microsoft.sqlserver.jdbc.SQLServerDriver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "jdbc:sqlserver://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.datasource.replicated.ro.driver-class-name", "com.microsoft.sqlserver.jdbc.SQLServerDriver") + .containsEntry("spring.datasource.replicated.ro.password", "testro-password") + .containsEntry("spring.datasource.replicated.ro.url", "jdbc:sqlserver://testro-host:testro-port/testro-database") + .containsEntry("spring.datasource.replicated.ro.username", "testro-username") + .containsEntry("spring.datasource.replicated.name", "dbrw"); + } + + @Test + @DisplayName("gives precedence to jdbc-url") + void testJdbcURL() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-jdbc-url", "testrw-jdbc-url").withEntry("ro-jdbc-url", "testro-jdbc-url")) + ); + new SqlServerReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.replicated.rw.driver-class-name", "com.microsoft.sqlserver.jdbc.SQLServerDriver") + .containsEntry("spring.datasource.replicated.rw.password", "testrw-password") + .containsEntry("spring.datasource.replicated.rw.url", "testrw-jdbc-url") + .containsEntry("spring.datasource.replicated.rw.username", "testrw-username") + .containsEntry("spring.datasource.replicated.ro.driver-class-name", "com.microsoft.sqlserver.jdbc.SQLServerDriver") + .containsEntry("spring.datasource.replicated.ro.password", "testro-password") + .containsEntry("spring.datasource.replicated.ro.url", "testro-jdbc-url") + .containsEntry("spring.datasource.replicated.ro.username", "testro-username") + .containsEntry("spring.datasource.replicated.name", "dbrw"); + } + + @Test + @DisplayName("composes r2dbc url from host port and database") + void testR2dbc() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + new SqlServerReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "r2dbc:sqlserver://testrw-host:testrw-port/testrw-database") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.ro.password", "testro-password") + .containsEntry("spring.r2dbc.replicated.ro.url", "r2dbc:sqlserver://testro-host:testro-port/testro-database") + .containsEntry("spring.r2dbc.replicated.ro.username", "testro-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("gives precedence to r2dbc-url") + void testR2dbcURL() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret.withEntry("rw-r2dbc-url", "testrw-r2dbc-url").withEntry("ro-r2dbc-url", "testro-r2dbc-url")) + ); + new SqlServerReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.replicated.rw.password", "testrw-password") + .containsEntry("spring.r2dbc.replicated.rw.url", "testrw-r2dbc-url") + .containsEntry("spring.r2dbc.replicated.rw.username", "testrw-username") + .containsEntry("spring.r2dbc.replicated.ro.password", "testro-password") + .containsEntry("spring.r2dbc.replicated.ro.url", "testro-r2dbc-url") + .containsEntry("spring.r2dbc.replicated.ro.username", "testro-username") + .containsEntry("spring.r2dbc.replicated.name", "dbrw"); + } + + @Test + @DisplayName("can be disabled") + void disabled() { + Bindings bindings = new Bindings( + new Binding("dbrw", Paths.get("dbrw-path"), secret) + ); + environment.setProperty("org.springframework.cloud.bindings.boot.sqlserver-replicated.enable", "false"); + + new SqlServerReplicatedBindingsPropertiesProcessor().process(environment, bindings, properties); + + assertThat(properties).isEmpty(); + } +}