From 6134ff19f94f73bb262cab2d28d18b2c55db4121 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Mon, 6 Jul 2020 22:59:44 +0200 Subject: [PATCH] Add auto-configuration for Neo4j driver This commit adds the support for creating a managed instance of the Neo4j Java driver. The low-level support for Neo4j is helpful in situations where the high-level abstraction of Spring Data Neo4j is not needed. See gh-22301 --- .../DocumentConfigurationProperties.java | 5 +- .../spring-boot-autoconfigure/build.gradle | 1 + .../neo4j/Neo4jAutoConfiguration.java | 193 ++++++++++ .../autoconfigure/neo4j/Neo4jProperties.java | 357 ++++++++++++++++++ .../neo4j/Neo4jSpringJclLogging.java | 103 +++++ .../autoconfigure/neo4j/package-info.java | 20 + .../main/resources/META-INF/spring.factories | 1 + ...eo4jAutoConfigurationIntegrationTests.java | 77 ++++ .../neo4j/Neo4jAutoConfigurationTests.java | 94 +++++ .../neo4j/Neo4jPropertiesTests.java | 319 ++++++++++++++++ .../neo4j/TestServerAddressResolver.java | 37 ++ .../spring-boot-dependencies/build.gradle | 7 + 12 files changed, 1212 insertions(+), 2 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jSpringJclLogging.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jPropertiesTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/TestServerAddressResolver.java diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java index 9b30e1e4ed..44b0119a2f 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java @@ -34,6 +34,7 @@ import org.springframework.boot.build.context.properties.DocumentOptions.Builder * {@link Task} used to document auto-configuration classes. * * @author Andy Wilkinson + * @author Michael J. Simons */ public class DocumentConfigurationProperties extends DefaultTask { @@ -80,8 +81,8 @@ public class DocumentConfigurationProperties extends DefaultTask { .addSection("security").withKeyPrefixes("spring.security", "spring.ldap", "spring.session") .addSection("data-migration").withKeyPrefixes("spring.flyway", "spring.liquibase").addSection("data") .withKeyPrefixes("spring.couchbase", "spring.elasticsearch", "spring.h2", "spring.influx", - "spring.mongodb", "spring.redis", "spring.dao", "spring.data", "spring.datasource", - "spring.jooq", "spring.jdbc", "spring.jpa", "spring.r2dbc") + "spring.mongodb", "spring.neo4j", "spring.redis", "spring.dao", "spring.data", + "spring.datasource", "spring.jooq", "spring.jdbc", "spring.jpa", "spring.r2dbc") .addOverride("spring.datasource.dbcp2", "Commons DBCP2 specific settings bound to an instance of DBCP2's BasicDataSource") .addOverride("spring.datasource.tomcat", diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index ef0a410fb2..f2c4dd3933 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -180,6 +180,7 @@ dependencies { testImplementation("org.testcontainers:couchbase") testImplementation("org.testcontainers:elasticsearch") testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:neo4j") testImplementation("org.testcontainers:testcontainers") testImplementation("org.yaml:snakeyaml") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java new file mode 100644 index 0000000000..ef9b6c9dd6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java @@ -0,0 +1,193 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import java.io.File; +import java.net.URI; +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import org.neo4j.driver.AuthToken; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Config; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.internal.Scheme; +import org.neo4j.driver.net.ServerAddressResolver; + +import org.springframework.beans.BeanUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +/** + * Automatic configuration of Neo4j's Java Driver. + * + * @author Michael J. Simons + * @since 2.4.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(Driver.class) +@EnableConfigurationProperties(Neo4jProperties.class) +public class Neo4jAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(Driver.class) + Driver neo4jDriver(Neo4jProperties properties) { + AuthToken authToken = asAuthToken(properties.getAuthentication()); + Config config = asDriverConfig(properties); + return GraphDatabase.driver(properties.getUri(), authToken, config); + } + + static AuthToken asAuthToken(Neo4jProperties.Authentication authentication) { + String username = authentication.getUsername(); + String password = authentication.getPassword(); + String kerberosTicket = authentication.getKerberosTicket(); + String realm = authentication.getRealm(); + + boolean hasUsername = StringUtils.hasText(username); + boolean hasPassword = StringUtils.hasText(password); + boolean hasKerberosTicket = StringUtils.hasText(kerberosTicket); + + if (hasUsername && hasKerberosTicket) { + throw new InvalidConfigurationPropertyValueException("org.neo4j.driver.authentication", + "username=" + username + ",kerberos-ticket=" + kerberosTicket, + "Cannot specify both username and kerberos ticket."); + } + + if (hasUsername && hasPassword) { + return AuthTokens.basic(username, password, realm); + } + + if (hasKerberosTicket) { + return AuthTokens.kerberos(kerberosTicket); + } + + return AuthTokens.none(); + } + + static Config asDriverConfig(Neo4jProperties properties) { + Config.ConfigBuilder builder = Config.builder(); + applyTo(builder, properties.getPool()); + URI uri = properties.getUri(); + String scheme = (uri != null) ? uri.getScheme() : "bolt"; + applyTo(builder, properties.getConfig(), isSimpleScheme(scheme)); + + return builder.withLogging(new Neo4jSpringJclLogging()).build(); + } + + static boolean isSimpleScheme(String scheme) { + String lowerCaseScheme = scheme.toLowerCase(Locale.ENGLISH); + try { + Scheme.validateScheme(lowerCaseScheme); + } + catch (IllegalArgumentException ex) { + throw new IllegalArgumentException(String.format("'%s' is not a supported scheme.", scheme)); + } + + return lowerCaseScheme.equals("bolt") || lowerCaseScheme.equals("neo4j"); + } + + private static void applyTo(Config.ConfigBuilder builder, Neo4jProperties.PoolSettings poolSettings) { + if (poolSettings.isLogLeakedSessions()) { + builder.withLeakedSessionsLogging(); + } + builder.withMaxConnectionPoolSize(poolSettings.getMaxConnectionPoolSize()); + Duration idleTimeBeforeConnectionTest = poolSettings.getIdleTimeBeforeConnectionTest(); + if (idleTimeBeforeConnectionTest != null) { + builder.withConnectionLivenessCheckTimeout(idleTimeBeforeConnectionTest.toMillis(), TimeUnit.MILLISECONDS); + } + builder.withMaxConnectionLifetime(poolSettings.getMaxConnectionLifetime().toMillis(), TimeUnit.MILLISECONDS); + builder.withConnectionAcquisitionTimeout(poolSettings.getConnectionAcquisitionTimeout().toMillis(), + TimeUnit.MILLISECONDS); + + if (poolSettings.isMetricsEnabled()) { + builder.withDriverMetrics(); + } + else { + builder.withoutDriverMetrics(); + } + } + + private static void applyTo(Config.ConfigBuilder builder, Neo4jProperties.DriverSettings driverSettings, + boolean withEncryptionAndTrustSettings) { + if (withEncryptionAndTrustSettings) { + applyEncryptionAndTrustSettings(builder, driverSettings); + } + + builder.withConnectionTimeout(driverSettings.getConnectionTimeout().toMillis(), TimeUnit.MILLISECONDS); + builder.withMaxTransactionRetryTime(driverSettings.getMaxTransactionRetryTime().toMillis(), + TimeUnit.MILLISECONDS); + + Class serverAddressResolverClass = driverSettings + .getServerAddressResolverClass(); + if (serverAddressResolverClass != null) { + builder.withResolver(BeanUtils.instantiateClass(serverAddressResolverClass)); + } + } + + private static void applyEncryptionAndTrustSettings(Config.ConfigBuilder builder, + Neo4jProperties.DriverSettings driverSettings) { + if (driverSettings.isEncrypted()) { + builder.withEncryption(); + } + else { + builder.withoutEncryption(); + } + builder.withTrustStrategy(toInternalRepresentation(driverSettings.getTrustSettings())); + } + + static Config.TrustStrategy toInternalRepresentation(Neo4jProperties.TrustSettings trustSettings) { + String propertyName = "org.neo4j.driver.config.trust-settings"; + + Config.TrustStrategy internalRepresentation; + Neo4jProperties.TrustSettings.Strategy strategy = trustSettings.getStrategy(); + switch (strategy) { + case TRUST_ALL_CERTIFICATES: + internalRepresentation = Config.TrustStrategy.trustAllCertificates(); + break; + case TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: + internalRepresentation = Config.TrustStrategy.trustSystemCertificates(); + break; + case TRUST_CUSTOM_CA_SIGNED_CERTIFICATES: + File certFile = trustSettings.getCertFile(); + if (certFile == null || !certFile.isFile()) { + throw new InvalidConfigurationPropertyValueException(propertyName, strategy.name(), + "Configured trust strategy requires a certificate file."); + } + internalRepresentation = Config.TrustStrategy.trustCustomCertificateSignedBy(certFile); + break; + default: + throw new InvalidConfigurationPropertyValueException(propertyName, strategy.name(), "Unknown strategy."); + } + + if (trustSettings.isHostnameVerificationEnabled()) { + internalRepresentation.withHostnameVerification(); + } + else { + internalRepresentation.withoutHostnameVerification(); + } + + return internalRepresentation; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jProperties.java new file mode 100644 index 0000000000..294fdca2f2 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jProperties.java @@ -0,0 +1,357 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import java.io.File; +import java.net.URI; +import java.time.Duration; + +import org.neo4j.driver.net.ServerAddressResolver; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Used to configure an instance of the {@link org.neo4j.driver.Driver Neo4j-Java-Driver}. + * + * @author Michael J. Simons + * @since 2.4.0 + */ +@ConfigurationProperties(prefix = "spring.neo4j") +public class Neo4jProperties { + + /** + * Uri this driver should connect to. The driver supports bolt or neo4j as schemes. + */ + private URI uri = URI.create("bolt://localhost:7687"); + + /** + * Authentication the driver is supposed to use. Maybe null. + */ + private Authentication authentication = new Authentication(); + + /** + * Configuration of the connection pool. + */ + private PoolSettings pool = new PoolSettings(); + + /** + * Detailed configuration of the driver. + */ + private DriverSettings config = new DriverSettings(); + + public URI getUri() { + return this.uri; + } + + public void setUri(URI uri) { + this.uri = uri; + } + + public Authentication getAuthentication() { + return this.authentication; + } + + public void setAuthentication(Authentication authentication) { + this.authentication = authentication; + } + + public PoolSettings getPool() { + return this.pool; + } + + public void setPool(PoolSettings pool) { + this.pool = pool; + } + + public DriverSettings getConfig() { + return this.config; + } + + public void setConfig(DriverSettings config) { + this.config = config; + } + + public static class Authentication { + + /** + * Login of the user connecting to the database. + */ + private String username; + + /** + * Password of the user connecting to the database. + */ + private String password; + + /** + * Realm to connect to. + */ + private String realm; + + /** + * Kerberos ticket for connecting to the database. Mutual exclusive with a given + * username. + */ + private String kerberosTicket; + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getRealm() { + return this.realm; + } + + public void setRealm(String realm) { + this.realm = realm; + } + + public String getKerberosTicket() { + return this.kerberosTicket; + } + + public void setKerberosTicket(String kerberosTicket) { + this.kerberosTicket = kerberosTicket; + } + + } + + public static class PoolSettings { + + /** + * Flag, if metrics are enabled. + */ + private boolean metricsEnabled = false; + + /** + * Flag, if leaked sessions logging is enabled. + */ + private boolean logLeakedSessions = false; + + /** + * Maximum amount of connections in the connection pool towards a single database. + */ + private int maxConnectionPoolSize = org.neo4j.driver.internal.async.pool.PoolSettings.DEFAULT_MAX_CONNECTION_POOL_SIZE; + + /** + * Pooled connections that have been idle in the pool for longer than this timeout + * will be tested before they are used again. + */ + private Duration idleTimeBeforeConnectionTest; + + /** + * Pooled connections older than this threshold will be closed and removed from + * the pool. + */ + private Duration maxConnectionLifetime = Duration + .ofMillis(org.neo4j.driver.internal.async.pool.PoolSettings.DEFAULT_MAX_CONNECTION_LIFETIME); + + /** + * Acquisition of new connections will be attempted for at most configured + * timeout. + */ + private Duration connectionAcquisitionTimeout = Duration + .ofMillis(org.neo4j.driver.internal.async.pool.PoolSettings.DEFAULT_CONNECTION_ACQUISITION_TIMEOUT); + + public boolean isLogLeakedSessions() { + return this.logLeakedSessions; + } + + public void setLogLeakedSessions(boolean logLeakedSessions) { + this.logLeakedSessions = logLeakedSessions; + } + + public int getMaxConnectionPoolSize() { + return this.maxConnectionPoolSize; + } + + public void setMaxConnectionPoolSize(int maxConnectionPoolSize) { + this.maxConnectionPoolSize = maxConnectionPoolSize; + } + + public Duration getIdleTimeBeforeConnectionTest() { + return this.idleTimeBeforeConnectionTest; + } + + public void setIdleTimeBeforeConnectionTest(Duration idleTimeBeforeConnectionTest) { + this.idleTimeBeforeConnectionTest = idleTimeBeforeConnectionTest; + } + + public Duration getMaxConnectionLifetime() { + return this.maxConnectionLifetime; + } + + public void setMaxConnectionLifetime(Duration maxConnectionLifetime) { + this.maxConnectionLifetime = maxConnectionLifetime; + } + + public Duration getConnectionAcquisitionTimeout() { + return this.connectionAcquisitionTimeout; + } + + public void setConnectionAcquisitionTimeout(Duration connectionAcquisitionTimeout) { + this.connectionAcquisitionTimeout = connectionAcquisitionTimeout; + } + + public boolean isMetricsEnabled() { + return this.metricsEnabled; + } + + public void setMetricsEnabled(boolean metricsEnabled) { + this.metricsEnabled = metricsEnabled; + } + + } + + public static class DriverSettings { + + /** + * Flag, if the driver should use encrypted traffic. + */ + private boolean encrypted = false; + + /** + * Specify how to determine the authenticity of an encryption certificate provided + * by the Neo4j instance we are connecting to. + */ + private TrustSettings trustSettings = new TrustSettings(); + + /** + * Specify socket connection timeout. + */ + private Duration connectionTimeout = Duration.ofSeconds(30); + + /** + * Specify the maximum time transactions are allowed to retry. + */ + private Duration maxTransactionRetryTime = Duration + .ofMillis(org.neo4j.driver.internal.retry.RetrySettings.DEFAULT.maxRetryTimeMs()); + + /** + * Specify a custom server address resolver used by the routing driver to resolve + * the initial address used to create the driver. + */ + private Class serverAddressResolverClass; + + public boolean isEncrypted() { + return this.encrypted; + } + + public void setEncrypted(boolean encrypted) { + this.encrypted = encrypted; + } + + public TrustSettings getTrustSettings() { + return this.trustSettings; + } + + public void setTrustSettings(TrustSettings trustSettings) { + this.trustSettings = trustSettings; + } + + public Duration getConnectionTimeout() { + return this.connectionTimeout; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public Duration getMaxTransactionRetryTime() { + return this.maxTransactionRetryTime; + } + + public void setMaxTransactionRetryTime(Duration maxTransactionRetryTime) { + this.maxTransactionRetryTime = maxTransactionRetryTime; + } + + public Class getServerAddressResolverClass() { + return this.serverAddressResolverClass; + } + + public void setServerAddressResolverClass(Class serverAddressResolverClass) { + this.serverAddressResolverClass = serverAddressResolverClass; + } + + } + + public static class TrustSettings { + + public enum Strategy { + + TRUST_ALL_CERTIFICATES, + + TRUST_CUSTOM_CA_SIGNED_CERTIFICATES, + + TRUST_SYSTEM_CA_SIGNED_CERTIFICATES + + } + + /** + * Configures the strategy to use use. + */ + private Strategy strategy = Strategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES; + + /** + * File of the certificate to use. + */ + private File certFile; + + /** + * Flag, if hostname verification is used. + */ + private boolean hostnameVerificationEnabled = false; + + public Strategy getStrategy() { + return this.strategy; + } + + public void setStrategy(Strategy strategy) { + this.strategy = strategy; + } + + public File getCertFile() { + return this.certFile; + } + + public void setCertFile(File certFile) { + this.certFile = certFile; + } + + public boolean isHostnameVerificationEnabled() { + return this.hostnameVerificationEnabled; + } + + public void setHostnameVerificationEnabled(boolean hostnameVerificationEnabled) { + this.hostnameVerificationEnabled = hostnameVerificationEnabled; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jSpringJclLogging.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jSpringJclLogging.java new file mode 100644 index 0000000000..8e6adf0c83 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jSpringJclLogging.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.neo4j.driver.Logger; +import org.neo4j.driver.Logging; + +/** + * Shim to use Spring JCL implementation, delegating all the hard work of deciding the + * underlying system to Spring and Spring Boot. + * + * @author Michael J. Simons + */ +class Neo4jSpringJclLogging implements Logging { + + /** + * This prefix gets added to the log names the driver requests to add some namespace + * around it in a bigger application scenario. + */ + private static final String AUTOMATIC_PREFIX = "org.neo4j.driver."; + + @Override + public Logger getLog(String name) { + + String requestedLog = name; + if (!requestedLog.startsWith(AUTOMATIC_PREFIX)) { + requestedLog = AUTOMATIC_PREFIX + name; + } + Log springJclLog = LogFactory.getLog(requestedLog); + return new SpringJclLogger(springJclLog); + } + + static final class SpringJclLogger implements Logger { + + private final Log delegate; + + SpringJclLogger(Log delegate) { + this.delegate = delegate; + } + + @Override + public void error(String message, Throwable cause) { + this.delegate.error(message, cause); + } + + @Override + public void info(String format, Object... params) { + this.delegate.info(String.format(format, params)); + } + + @Override + public void warn(String format, Object... params) { + this.delegate.warn(String.format(format, params)); + } + + @Override + public void warn(String message, Throwable cause) { + this.delegate.warn(message, cause); + } + + @Override + public void debug(String format, Object... params) { + if (isDebugEnabled()) { + this.delegate.debug(String.format(format, params)); + } + } + + @Override + public void trace(String format, Object... params) { + if (isTraceEnabled()) { + this.delegate.trace(String.format(format, params)); + } + } + + @Override + public boolean isTraceEnabled() { + return this.delegate.isTraceEnabled(); + } + + @Override + public boolean isDebugEnabled() { + return this.delegate.isDebugEnabled(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/package-info.java new file mode 100644 index 0000000000..d47e98efc1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Neo4j. + */ +package org.springframework.boot.autoconfigure.neo4j; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index eb6f3f7f90..144b482523 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -100,6 +100,7 @@ org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfigura org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\ org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\ org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\ +org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration,\ org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\ org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\ org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java new file mode 100644 index 0000000000..91112fda4e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Result; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.testcontainers.containers.Neo4jContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Michael J. Simons + */ +@SpringBootTest +@Testcontainers(disabledWithoutDocker = true) +class Neo4jAutoConfigurationIntegrationTests { + + @Container + private static Neo4jContainer neo4jServer = new Neo4jContainer<>("neo4j:4.0"); + + @DynamicPropertySource + static void neo4jProperties(DynamicPropertyRegistry registry) { + registry.add("spring.neo4j.uri", neo4jServer::getBoltUrl); + registry.add("spring.neo4j.authentication.username", () -> "neo4j"); + registry.add("spring.neo4j.authentication.password", neo4jServer::getAdminPassword); + } + + private final Driver driver; + + @Autowired + Neo4jAutoConfigurationIntegrationTests(Driver driver) { + this.driver = driver; + } + + @Test + void ensureDriverIsOpen() { + + try (Session session = this.driver.session(); Transaction tx = session.beginTransaction()) { + Result statementResult = tx.run("MATCH (n:Thing) RETURN n LIMIT 1"); + assertThat(statementResult.hasNext()).isFalse(); + tx.commit(); + } + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(Neo4jAutoConfiguration.class) + static class TestConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java new file mode 100644 index 0000000000..d614a098e1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.neo4j.driver.Driver; +import org.neo4j.driver.exceptions.ClientException; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.when; + +/** + * @author Michael J. Simons + */ +class Neo4jAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(Neo4jAutoConfiguration.class)); + + @Test + void shouldRequireAllNeededClasses() { + + this.contextRunner.withPropertyValues("spring.neo4j.uri=bolt://localhost:4711") + .withClassLoader(new FilteredClassLoader(Driver.class)) + .run((ctx) -> assertThat(ctx).doesNotHaveBean(Driver.class)); + } + + @Test + void shouldNotRequireUri() { + + this.contextRunner.run((ctx) -> assertThat(ctx).hasSingleBean(Driver.class)); + } + + @Test + void shouldCreateDriver() { + + this.contextRunner.withPropertyValues("spring.neo4j.uri=bolt://localhost:4711") + .run((ctx) -> assertThat(ctx).hasSingleBean(Driver.class)); + } + + /** + * These tests assert correct configuration behaviour for cases in which one of the + * "advanced" schemes is used to configure the driver. If any of the schemes is used, + * than a contradicting explicit configuration will throw an error. + * @param scheme The scheme to test. + */ + @ParameterizedTest + @ValueSource(strings = { "bolt+s", "bolt+ssc", "neo4j+s", "neo4j+ssc" }) + void schemesShouldBeApplied(String scheme) { + + this.contextRunner.withPropertyValues("spring.neo4j.uri=" + scheme + "://localhost:4711").run((ctx) -> { + assertThat(ctx).hasSingleBean(Driver.class); + + Driver driver = ctx.getBean(Driver.class); + assertThat(driver.isEncrypted()).isTrue(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class WithDriver { + + @Bean + Driver driver() { + Driver driver = mock(Driver.class); + when(driver.metrics()).thenThrow(ClientException.class); + return driver; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jPropertiesTests.java new file mode 100644 index 0000000000..a090f47044 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jPropertiesTests.java @@ -0,0 +1,319 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.time.Duration; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Config; +import org.neo4j.driver.internal.retry.RetrySettings; + +import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Authentication; +import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.DriverSettings; +import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.PoolSettings; +import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.TrustSettings; +import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.TrustSettings.Strategy; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Michael J. Simons + */ +class Neo4jPropertiesTests { + + private static void assertDuration(Duration duration, long expectedValueInMillis) { + if (expectedValueInMillis == org.neo4j.driver.internal.async.pool.PoolSettings.NOT_CONFIGURED) { + assertThat(duration).isNull(); + } + else { + assertThat(duration.toMillis()).isEqualTo(expectedValueInMillis); + } + } + + @Test + void shouldAllowEmptyListOfURIs() { + Neo4jProperties driverProperties = new Neo4jProperties(); + assertThat(driverProperties.getAuthentication()).isNotNull(); + } + + @Test + void noAuthenticationShouldWork() { + Authentication authentication = new Authentication(); + assertThat(Neo4jAutoConfiguration.asAuthToken(authentication)).isEqualTo(AuthTokens.none()); + } + + @Test + void basicAuthShouldWork() { + Authentication authentication = new Authentication(); + authentication.setUsername("Farin"); + authentication.setPassword("Urlaub"); + + assertThat(Neo4jAutoConfiguration.asAuthToken(authentication)).isEqualTo(AuthTokens.basic("Farin", "Urlaub")); + } + + @Test + void basicAuthWithRealmShouldWork() { + Authentication authentication = new Authentication(); + authentication.setUsername("Farin"); + authentication.setPassword("Urlaub"); + authentication.setRealm("Die Ärzte"); + + assertThat(Neo4jAutoConfiguration.asAuthToken(authentication)) + .isEqualTo(AuthTokens.basic("Farin", "Urlaub", "Die Ärzte")); + } + + @Test + void kerberosAuthShouldWork() { + Authentication authentication = new Authentication(); + authentication.setKerberosTicket("AABBCCDDEE"); + + assertThat(Neo4jAutoConfiguration.asAuthToken(authentication)).isEqualTo(AuthTokens.kerberos("AABBCCDDEE")); + } + + @Test + void ambiguousShouldNotBeAllowed() { + Authentication authentication = new Authentication(); + authentication.setUsername("Farin"); + authentication.setKerberosTicket("AABBCCDDEE"); + + assertThatExceptionOfType(InvalidConfigurationPropertyValueException.class) + .isThrownBy(() -> Neo4jAutoConfiguration.asAuthToken(authentication)).withMessage( + "Property org.neo4j.driver.authentication with value 'username=Farin,kerberos-ticket=AABBCCDDEE' is invalid: Cannot specify both username and kerberos ticket."); + } + + @Test + void poolSettingsShouldDefaultToDriversValues() { + Config defaultConfig = Config.defaultConfig(); + + Neo4jProperties driverProperties = new Neo4jProperties(); + + PoolSettings poolSettings = driverProperties.getPool(); + assertThat(poolSettings.isLogLeakedSessions()).isEqualTo(defaultConfig.logLeakedSessions()); + assertThat(poolSettings.getMaxConnectionPoolSize()).isEqualTo(defaultConfig.maxConnectionPoolSize()); + assertDuration(poolSettings.getIdleTimeBeforeConnectionTest(), defaultConfig.idleTimeBeforeConnectionTest()); + assertDuration(poolSettings.getMaxConnectionLifetime(), defaultConfig.maxConnectionLifetimeMillis()); + assertDuration(poolSettings.getConnectionAcquisitionTimeout(), + defaultConfig.connectionAcquisitionTimeoutMillis()); + assertThat(poolSettings.isMetricsEnabled()).isFalse(); + } + + @Test + void logLeakedSessionsSettingsShouldWork() { + Neo4jProperties driverProperties; + + driverProperties = new Neo4jProperties(); + driverProperties.getPool().setLogLeakedSessions(true); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).logLeakedSessions()).isTrue(); + + driverProperties = new Neo4jProperties(); + driverProperties.getPool().setLogLeakedSessions(false); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).logLeakedSessions()).isFalse(); + } + + @Test + void maxConnectionPoolSizeSettingsShouldWork() { + Neo4jProperties driverProperties = new Neo4jProperties(); + driverProperties.getPool().setMaxConnectionPoolSize(4711); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).maxConnectionPoolSize()).isEqualTo(4711); + } + + @Test + void idleTimeBeforeConnectionTestSettingsShouldWork() { + Neo4jProperties driverProperties; + + driverProperties = new Neo4jProperties(); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).idleTimeBeforeConnectionTest()) + .isEqualTo(-1); + + driverProperties = new Neo4jProperties(); + driverProperties.getPool().setIdleTimeBeforeConnectionTest(Duration.ofSeconds(23)); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).idleTimeBeforeConnectionTest()) + .isEqualTo(23_000); + } + + @Test + void connectionAcquisitionTimeoutSettingsShouldWork() { + Neo4jProperties driverProperties = new Neo4jProperties(); + driverProperties.getPool().setConnectionAcquisitionTimeout(Duration.ofSeconds(23)); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).connectionAcquisitionTimeoutMillis()) + .isEqualTo(23_000); + } + + @Test + void enableMetricsShouldWork() { + Neo4jProperties driverProperties = new Neo4jProperties(); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).isMetricsEnabled()).isFalse(); + + driverProperties.getPool().setMetricsEnabled(true); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).isMetricsEnabled()).isTrue(); + } + + @Test + void driverSettingsShouldDefaultToDriversValues() { + Config defaultConfig = Config.defaultConfig(); + + Neo4jProperties driverProperties = new Neo4jProperties(); + + DriverSettings driverSettings = driverProperties.getConfig(); + assertThat(driverSettings.isEncrypted()).isEqualTo(defaultConfig.encrypted()); + assertThat(driverSettings.getTrustSettings().getStrategy().name()) + .isEqualTo(defaultConfig.trustStrategy().strategy().name()); + assertDuration(driverSettings.getConnectionTimeout(), defaultConfig.connectionTimeoutMillis()); + assertDuration(driverSettings.getMaxTransactionRetryTime(), RetrySettings.DEFAULT.maxRetryTimeMs()); + assertThat(driverSettings.getServerAddressResolverClass()).isNull(); + } + + @Test + void encryptedSettingsShouldWork() { + Neo4jProperties driverProperties; + + driverProperties = new Neo4jProperties(); + driverProperties.getConfig().setEncrypted(true); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).encrypted()).isTrue(); + + driverProperties = new Neo4jProperties(); + driverProperties.getConfig().setEncrypted(false); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).encrypted()).isFalse(); + } + + @Test + void trustSettingsShouldWork() { + Neo4jProperties driverProperties = new Neo4jProperties(); + TrustSettings trustSettings = new TrustSettings(); + trustSettings.setStrategy(Strategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES); + driverProperties.getConfig().setTrustSettings(trustSettings); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).trustStrategy().strategy()) + .isEqualTo(Config.TrustStrategy.Strategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES); + } + + @Test + void connectionTimeoutSettingsShouldWork() { + Neo4jProperties driverProperties = new Neo4jProperties(); + driverProperties.getConfig().setConnectionTimeout(Duration.ofSeconds(23)); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).connectionTimeoutMillis()).isEqualTo(23_000); + } + + @Test + @Disabled("The internal driver has no means of retrieving that value back again") + void maxTransactionRetryTimeSettingsShouldWork() { + DriverSettings driverSettings = new DriverSettings(); + driverSettings.setMaxTransactionRetryTime(Duration.ofSeconds(23)); + } + + @Test + void serverAddressResolverClassSettingsShouldWork() { + Neo4jProperties driverProperties = new Neo4jProperties(); + driverProperties.getConfig().setServerAddressResolverClass(TestServerAddressResolver.class); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).resolver()).isNotNull() + .isInstanceOf(TestServerAddressResolver.class); + } + + @Test + void shouldUseSpringJclLogging() { + Neo4jProperties driverProperties = new Neo4jProperties(); + assertThat(Neo4jAutoConfiguration.asDriverConfig(driverProperties).logging()).isNotNull() + .isInstanceOf(Neo4jSpringJclLogging.class); + } + + @Test + void trustAllCertificatesShouldWork() { + TrustSettings settings = new TrustSettings(); + settings.setStrategy(Strategy.TRUST_ALL_CERTIFICATES); + + assertThat(Neo4jAutoConfiguration.toInternalRepresentation(settings).strategy()) + .isEqualTo(Config.TrustStrategy.Strategy.TRUST_ALL_CERTIFICATES); + } + + @Test + void shouldEnableHostnameVerification() { + TrustSettings settings = new TrustSettings(); + settings.setStrategy(Strategy.TRUST_ALL_CERTIFICATES); + settings.setHostnameVerificationEnabled(true); + + assertThat(Neo4jAutoConfiguration.toInternalRepresentation(settings).isHostnameVerificationEnabled()).isTrue(); + } + + @Test + void trustSystemCertificatesShouldWork() { + + TrustSettings settings = new TrustSettings(); + settings.setStrategy(Strategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES); + + assertThat(Neo4jAutoConfiguration.toInternalRepresentation(settings).strategy()) + .isEqualTo(Config.TrustStrategy.Strategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES); + } + + @Test + void trustCustomCertificatesShouldWork() throws IOException { + File certFile = File.createTempFile("neo4j-driver", ".cert"); + + TrustSettings settings = new TrustSettings(); + settings.setStrategy(Strategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES); + settings.setCertFile(certFile); + + Config.TrustStrategy trustStrategy = Neo4jAutoConfiguration.toInternalRepresentation(settings); + assertThat(trustStrategy.strategy()) + .isEqualTo(Config.TrustStrategy.Strategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES); + assertThat(trustStrategy.certFile()).isEqualTo(certFile); + } + + @Test + void trustCustomCertificatesShouldFailWithoutCertificate() { + TrustSettings settings = new TrustSettings(); + settings.setStrategy(Strategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES); + + assertThatExceptionOfType(InvalidConfigurationPropertyValueException.class) + .isThrownBy(() -> Neo4jAutoConfiguration.toInternalRepresentation(settings)).withMessage( + "Property org.neo4j.driver.config.trust-settings with value 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES' is invalid: Configured trust strategy requires a certificate file."); + } + + @Test + void shouldAssumeDefaultValuesForUrl() { + Neo4jProperties driverProperties = new Neo4jProperties(); + assertThat(driverProperties.getUri()).isEqualTo(URI.create("bolt://localhost:7687")); + } + + @ParameterizedTest + @ValueSource(strings = { "bolt", "Bolt", "neo4j", "Neo4J" }) + void shouldDetectSimpleSchemes(String aSimpleScheme) { + assertThat(Neo4jAutoConfiguration.isSimpleScheme(aSimpleScheme)).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { "bolt+s", "Bolt+ssc", "neo4j+s", "Neo4J+ssc" }) + void shouldDetectAdvancedSchemes(String anAdvancedScheme) { + assertThat(Neo4jAutoConfiguration.isSimpleScheme(anAdvancedScheme)).isFalse(); + } + + @ParameterizedTest + @ValueSource(strings = { "bolt+routing", "bolt+x", "neo4j+wth" }) + void shouldFailEarlyOnInvalidSchemes(String invalidScheme) { + assertThatIllegalArgumentException().isThrownBy(() -> Neo4jAutoConfiguration.isSimpleScheme(invalidScheme)) + .withMessage("'%s' is not a supported scheme.", invalidScheme); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/TestServerAddressResolver.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/TestServerAddressResolver.java new file mode 100644 index 0000000000..6c2f5dfa9d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/TestServerAddressResolver.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import java.util.Collections; +import java.util.Set; + +import org.neo4j.driver.net.ServerAddress; +import org.neo4j.driver.net.ServerAddressResolver; + +/** + * Resolver used only for configuration tests. + * + * @author Michael J. Simons + */ +class TestServerAddressResolver implements ServerAddressResolver { + + @Override + public Set resolve(ServerAddress address) { + return Collections.emptySet(); + } + +} diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d7f57c7934..b83d9a9f75 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1170,6 +1170,13 @@ bom { ] } } + library("Neo4j", "4.1.0") { + group("org.neo4j.driver") { + modules = [ + "neo4j-java-driver" + ] + } + } library("Neo4j OGM", "3.2.12") { group("org.neo4j") { modules = [