diff --git a/driver/src/main/java/org/neo4j/driver/Config.java b/driver/src/main/java/org/neo4j/driver/Config.java index 5065d0800b..4a5398225e 100644 --- a/driver/src/main/java/org/neo4j/driver/Config.java +++ b/driver/src/main/java/org/neo4j/driver/Config.java @@ -19,6 +19,7 @@ package org.neo4j.driver; import java.io.File; +import java.io.Serializable; import java.net.InetAddress; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -64,8 +65,10 @@ * @since 1.0 */ @Immutable -public class Config +public class Config implements Serializable { + private static final long serialVersionUID = -4496545746399601108L; + private static final Config EMPTY = builder().build(); /** User defined logging */ @@ -780,8 +783,10 @@ public Config build() /** * Control how the driver determines if it can trust the encryption certificates provided by the Neo4j instance it is connected to. */ - public static class TrustStrategy + public static class TrustStrategy implements Serializable { + private static final long serialVersionUID = -1631888096243987740L; + /** * The trust strategy that the driver supports */ diff --git a/driver/src/main/java/org/neo4j/driver/SessionConfig.java b/driver/src/main/java/org/neo4j/driver/SessionConfig.java index 9682ffe8f2..4286ad28ef 100644 --- a/driver/src/main/java/org/neo4j/driver/SessionConfig.java +++ b/driver/src/main/java/org/neo4j/driver/SessionConfig.java @@ -20,6 +20,7 @@ import org.reactivestreams.Subscription; +import java.io.Serializable; import java.util.Arrays; import java.util.Objects; import java.util.Optional; @@ -33,14 +34,16 @@ /** * The session configurations used to configure a session. */ -public class SessionConfig +public class SessionConfig implements Serializable { + private static final long serialVersionUID = 5773462156979050657L; + private static final SessionConfig EMPTY = builder().build(); private final Iterable bookmarks; private final AccessMode defaultAccessMode; private final String database; - private final Optional fetchSize; + private final Long fetchSize; private final String impersonatedUser; private SessionConfig( Builder builder ) @@ -123,7 +126,7 @@ public Optional database() */ public Optional fetchSize() { - return fetchSize; + return Optional.ofNullable( fetchSize ); } /** @@ -170,7 +173,7 @@ public String toString() */ public static class Builder { - private Optional fetchSize = Optional.empty(); + private Long fetchSize = null; private Iterable bookmarks = null; private AccessMode defaultAccessMode = AccessMode.WRITE; private String database = null; @@ -278,7 +281,7 @@ public Builder withDatabase( String database ) */ public Builder withFetchSize( long size ) { - this.fetchSize = Optional.of( assertValidFetchSize( size ) ); + this.fetchSize = assertValidFetchSize( size ); return this; } diff --git a/driver/src/main/java/org/neo4j/driver/TransactionConfig.java b/driver/src/main/java/org/neo4j/driver/TransactionConfig.java index 3932f80184..fee9943fb3 100644 --- a/driver/src/main/java/org/neo4j/driver/TransactionConfig.java +++ b/driver/src/main/java/org/neo4j/driver/TransactionConfig.java @@ -18,7 +18,9 @@ */ package org.neo4j.driver; +import java.io.Serializable; import java.time.Duration; +import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -61,17 +63,22 @@ * * @see Session */ -public class TransactionConfig +public class TransactionConfig implements Serializable { + private static final long serialVersionUID = -7954949878657177280L; + private static final TransactionConfig EMPTY = builder().build(); private final Duration timeout; - private final Map metadata; + private final Map metadata; + + // Values are not serializable, hence, we keep a transient volatile map of them around + private transient volatile Map convertedMetadata; private TransactionConfig( Builder builder ) { this.timeout = builder.timeout; - this.metadata = unmodifiableMap( builder.metadata ); + this.metadata = builder.metadata; } /** @@ -111,7 +118,20 @@ public Duration timeout() */ public Map metadata() { - return metadata; + Map result = this.convertedMetadata; + if ( result == null ) + { + synchronized ( this ) + { + result = this.convertedMetadata; + if ( result == null ) + { + this.convertedMetadata = unmodifiableMap( Extract.mapOfValues( this.metadata ) ); + result = this.convertedMetadata; + } + } + } + return result; } /** @@ -161,7 +181,7 @@ public String toString() public static class Builder { private Duration timeout; - private Map metadata = emptyMap(); + private Map metadata = emptyMap(); private Builder() { @@ -202,7 +222,8 @@ public Builder withTimeout( Duration timeout ) public Builder withMetadata( Map metadata ) { requireNonNull( metadata, "Transaction metadata should not be null" ); - this.metadata = Extract.mapOfValues( metadata ); + metadata.values().forEach( Extract::assertParameter ); // Just assert valid parameters but don't create a value map yet + this.metadata = new HashMap<>( metadata ); // Create a defensive copy return this; } diff --git a/driver/src/main/java/org/neo4j/driver/internal/InternalBookmark.java b/driver/src/main/java/org/neo4j/driver/internal/InternalBookmark.java index e0e7ad65b7..c4ac5cbab7 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/InternalBookmark.java +++ b/driver/src/main/java/org/neo4j/driver/internal/InternalBookmark.java @@ -18,6 +18,7 @@ */ package org.neo4j.driver.internal; +import java.io.Serializable; import java.util.Collections; import java.util.HashSet; import java.util.Objects; @@ -28,8 +29,10 @@ import static java.util.Objects.requireNonNull; -public final class InternalBookmark implements Bookmark +public final class InternalBookmark implements Bookmark, Serializable { + private static final long serialVersionUID = 8196096018245038950L; + private static final InternalBookmark EMPTY = new InternalBookmark( Collections.emptySet() ); private final Set values; diff --git a/driver/src/main/java/org/neo4j/driver/internal/SecuritySettings.java b/driver/src/main/java/org/neo4j/driver/internal/SecuritySettings.java index beeee31e59..4a6245b2f8 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/SecuritySettings.java +++ b/driver/src/main/java/org/neo4j/driver/internal/SecuritySettings.java @@ -19,6 +19,7 @@ package org.neo4j.driver.internal; import java.io.IOException; +import java.io.Serializable; import java.security.GeneralSecurityException; import org.neo4j.driver.Config; @@ -30,8 +31,10 @@ import static org.neo4j.driver.internal.Scheme.isSecurityScheme; import static org.neo4j.driver.internal.security.SecurityPlanImpl.insecure; -public class SecuritySettings +public class SecuritySettings implements Serializable { + private static final long serialVersionUID = 4494615367164106576L; + private static final boolean DEFAULT_ENCRYPTED = false; private static final Config.TrustStrategy DEFAULT_TRUST_STRATEGY = Config.TrustStrategy.trustSystemCertificates(); private static final SecuritySettings DEFAULT = new SecuritySettings( DEFAULT_ENCRYPTED, DEFAULT_TRUST_STRATEGY ); diff --git a/driver/src/main/java/org/neo4j/driver/internal/logging/ConsoleLogging.java b/driver/src/main/java/org/neo4j/driver/internal/logging/ConsoleLogging.java index 9de74576bd..bf6d55e436 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/logging/ConsoleLogging.java +++ b/driver/src/main/java/org/neo4j/driver/internal/logging/ConsoleLogging.java @@ -19,6 +19,7 @@ package org.neo4j.driver.internal.logging; import java.io.PrintWriter; +import java.io.Serializable; import java.io.StringWriter; import java.time.LocalDateTime; import java.util.Objects; @@ -39,8 +40,10 @@ * * @see Logging#console(Level) */ -public class ConsoleLogging implements Logging +public class ConsoleLogging implements Logging, Serializable { + private static final long serialVersionUID = 9205935204074879150L; + private final Level level; public ConsoleLogging( Level level ) diff --git a/driver/src/main/java/org/neo4j/driver/internal/logging/DevNullLogging.java b/driver/src/main/java/org/neo4j/driver/internal/logging/DevNullLogging.java index 47bf77fa9f..02158709a6 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/logging/DevNullLogging.java +++ b/driver/src/main/java/org/neo4j/driver/internal/logging/DevNullLogging.java @@ -18,11 +18,15 @@ */ package org.neo4j.driver.internal.logging; +import java.io.Serializable; + import org.neo4j.driver.Logger; import org.neo4j.driver.Logging; -public class DevNullLogging implements Logging +public class DevNullLogging implements Logging, Serializable { + private static final long serialVersionUID = -2632752338512373821L; + public static final Logging DEV_NULL_LOGGING = new DevNullLogging(); private DevNullLogging() @@ -34,4 +38,14 @@ public Logger getLog( String name ) { return DevNullLogger.DEV_NULL_LOGGER; } + + // Don't remove that apparently unused method. + // It is involved during deserialization after readObject on the new object. + // The returned value replaces the object read. + // An enum would be preferable, but would not be API compatible. + // Reference: https://docs.oracle.com/en/java/javase/17/docs/specs/serialization/input.html#the-readresolve-method andJoshua Bloch, Effective Java 3rd edition + private Object readResolve() + { + return DEV_NULL_LOGGING; + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/logging/JULogging.java b/driver/src/main/java/org/neo4j/driver/internal/logging/JULogging.java index a753a74458..9dffed957e 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/logging/JULogging.java +++ b/driver/src/main/java/org/neo4j/driver/internal/logging/JULogging.java @@ -18,6 +18,7 @@ */ package org.neo4j.driver.internal.logging; +import java.io.Serializable; import java.util.logging.Level; import org.neo4j.driver.Logger; @@ -29,8 +30,10 @@ * * @see Logging#javaUtilLogging(Level) */ -public class JULogging implements Logging +public class JULogging implements Logging, Serializable { + private static final long serialVersionUID = -1145576859241657833L; + private final Level loggingLevel; public JULogging( Level loggingLevel ) diff --git a/driver/src/main/java/org/neo4j/driver/internal/logging/Slf4jLogging.java b/driver/src/main/java/org/neo4j/driver/internal/logging/Slf4jLogging.java index 852faf9ce6..a5c92d5735 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/logging/Slf4jLogging.java +++ b/driver/src/main/java/org/neo4j/driver/internal/logging/Slf4jLogging.java @@ -20,6 +20,8 @@ import org.slf4j.LoggerFactory; +import java.io.Serializable; + import org.neo4j.driver.Logger; import org.neo4j.driver.Logging; @@ -29,8 +31,10 @@ * * @see Logging#slf4j() */ -public class Slf4jLogging implements Logging +public class Slf4jLogging implements Logging, Serializable { + private static final long serialVersionUID = 4120390028025944991L; + @Override public Logger getLog( String name ) { diff --git a/driver/src/main/java/org/neo4j/driver/internal/retry/RetrySettings.java b/driver/src/main/java/org/neo4j/driver/internal/retry/RetrySettings.java index 3d404062b0..b9f650e9b2 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/retry/RetrySettings.java +++ b/driver/src/main/java/org/neo4j/driver/internal/retry/RetrySettings.java @@ -18,8 +18,12 @@ */ package org.neo4j.driver.internal.retry; -public final class RetrySettings +import java.io.Serializable; + +public final class RetrySettings implements Serializable { + private static final long serialVersionUID = -2895062473220745239L; + public static final RetrySettings DEFAULT = new RetrySettings( ExponentialBackoffRetryLogic.DEFAULT_MAX_RETRY_TIME_MS ); diff --git a/driver/src/test/java/org/neo4j/driver/ConfigTest.java b/driver/src/test/java/org/neo4j/driver/ConfigTest.java index eaa86598dc..98592ad0cb 100644 --- a/driver/src/test/java/org/neo4j/driver/ConfigTest.java +++ b/driver/src/test/java/org/neo4j/driver/ConfigTest.java @@ -18,25 +18,38 @@ */ package org.neo4j.driver; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.junit.platform.commons.support.HierarchyTraversalMode; +import org.junit.platform.commons.support.ReflectionSupport; import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import org.neo4j.driver.internal.logging.ConsoleLogging; +import org.neo4j.driver.internal.logging.DevNullLogging; +import org.neo4j.driver.internal.logging.JULogging; +import org.neo4j.driver.internal.logging.Slf4jLogging; import org.neo4j.driver.net.ServerAddressResolver; +import org.neo4j.driver.util.TestUtil; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; -import static org.neo4j.driver.internal.RevocationStrategy.STRICT; import static org.neo4j.driver.internal.RevocationStrategy.NO_CHECKS; +import static org.neo4j.driver.internal.RevocationStrategy.STRICT; import static org.neo4j.driver.internal.RevocationStrategy.VERIFY_IF_PRESENT; import static org.neo4j.driver.internal.handlers.pulln.FetchSizeUtil.DEFAULT_FETCH_SIZE; @@ -71,7 +84,7 @@ void shouldChangeToTrustedCert() } @Test - void shouldSupportLivenessCheckTimeoutSetting() throws Throwable + void shouldSupportLivenessCheckTimeoutSetting() { Config config = Config.builder().withConnectionLivenessCheckTimeout( 42, TimeUnit.SECONDS ).build(); @@ -79,7 +92,7 @@ void shouldSupportLivenessCheckTimeoutSetting() throws Throwable } @Test - void shouldAllowZeroConnectionLivenessCheckTimeout() throws Throwable + void shouldAllowZeroConnectionLivenessCheckTimeout() { Config config = Config.builder().withConnectionLivenessCheckTimeout( 0, TimeUnit.SECONDS ).build(); @@ -87,7 +100,7 @@ void shouldAllowZeroConnectionLivenessCheckTimeout() throws Throwable } @Test - void shouldAllowNegativeConnectionLivenessCheckTimeout() throws Throwable + void shouldAllowNegativeConnectionLivenessCheckTimeout() { Config config = Config.builder().withConnectionLivenessCheckTimeout( -42, TimeUnit.SECONDS ).build(); @@ -101,7 +114,7 @@ void shouldHaveCorrectMaxConnectionLifetime() } @Test - void shouldSupportMaxConnectionLifetimeSetting() throws Throwable + void shouldSupportMaxConnectionLifetimeSetting() { Config config = Config.builder().withMaxConnectionLifetime( 42, TimeUnit.SECONDS ).build(); @@ -109,7 +122,7 @@ void shouldSupportMaxConnectionLifetimeSetting() throws Throwable } @Test - void shouldAllowZeroConnectionMaxConnectionLifetime() throws Throwable + void shouldAllowZeroConnectionMaxConnectionLifetime() { Config config = Config.builder().withMaxConnectionLifetime( 0, TimeUnit.SECONDS ).build(); @@ -117,7 +130,7 @@ void shouldAllowZeroConnectionMaxConnectionLifetime() throws Throwable } @Test - void shouldAllowNegativeConnectionMaxConnectionLifetime() throws Throwable + void shouldAllowNegativeConnectionMaxConnectionLifetime() { Config config = Config.builder().withMaxConnectionLifetime( -42, TimeUnit.SECONDS ).build(); @@ -299,7 +312,7 @@ void shouldNotAllowNullResolver() } @Test - void shouldDefaultToDefaultFetchSize() throws Throwable + void shouldDefaultToDefaultFetchSize() { Config config = Config.defaultConfig(); assertEquals( DEFAULT_FETCH_SIZE, config.fetchSize() ); @@ -307,30 +320,30 @@ void shouldDefaultToDefaultFetchSize() throws Throwable @ParameterizedTest @ValueSource( longs = {100, 1, 1000, Long.MAX_VALUE, -1} ) - void shouldChangeFetchSize( long value ) throws Throwable + void shouldChangeFetchSize( long value ) { Config config = Config.builder().withFetchSize( value ).build(); - assertThat( config.fetchSize(), equalTo( value ) ); + assertEquals( value, config.fetchSize() ); } @ParameterizedTest @ValueSource( longs = {0, -100, -2} ) - void shouldErrorWithIllegalFetchSize( long value ) throws Throwable + void shouldErrorWithIllegalFetchSize( long value ) { assertThrows( IllegalArgumentException.class, () -> Config.builder().withFetchSize( value ).build() ); } @ParameterizedTest @ValueSource( ints = {100, 1, 1000, Integer.MAX_VALUE} ) - void shouldChangeEventLoopThreads( int value ) throws Throwable + void shouldChangeEventLoopThreads( int value ) { Config config = Config.builder().withEventLoopThreads( value ).build(); - assertThat( config.eventLoopThreads(), equalTo( value ) ); + assertEquals( value, config.eventLoopThreads() ); } @ParameterizedTest @ValueSource( ints = {0, -100, -2} ) - void shouldErrorWithIllegalEventLoopThreadsSize( int value ) throws Throwable + void shouldErrorWithIllegalEventLoopThreadsSize( int value ) { assertThrows( IllegalArgumentException.class, () -> Config.builder().withEventLoopThreads( value ).build() ); } @@ -339,7 +352,7 @@ void shouldErrorWithIllegalEventLoopThreadsSize( int value ) throws Throwable void shouldChangeUserAgent() { Config config = Config.builder().withUserAgent( "AwesomeDriver" ).build(); - assertThat( config.userAgent(), equalTo( "AwesomeDriver" ) ); + assertEquals( "AwesomeDriver", config.userAgent() ); } @Test @@ -348,4 +361,87 @@ void shouldErrorWithInvalidUserAgent() assertThrows( IllegalArgumentException.class, () -> Config.builder().withUserAgent( null ).build() ); assertThrows( IllegalArgumentException.class, () -> Config.builder().withUserAgent( "" ).build() ); } + + @Nested + class SerializationTest + { + + @Test + void shouldSerialize() throws Exception + { + Config config = Config.builder() + .withMaxConnectionPoolSize( 123 ) + .withConnectionTimeout( 6543L, TimeUnit.MILLISECONDS ) + .withConnectionAcquisitionTimeout( 5432L, TimeUnit.MILLISECONDS ) + .withConnectionLivenessCheckTimeout( 4321L, TimeUnit.MILLISECONDS ) + .withMaxConnectionLifetime( 4711, TimeUnit.MILLISECONDS ) + .withMaxTransactionRetryTime( 3210L, TimeUnit.MILLISECONDS ) + .withFetchSize( 9876L ) + .withEventLoopThreads( 4 ) + .withoutEncryption() + .withTrustStrategy( Config.TrustStrategy.trustCustomCertificateSignedBy( new File( "doesntMatter" )) ) + .withUserAgent( "user-agent" ) + .withDriverMetrics() + .withRoutingTablePurgeDelay( 50000, TimeUnit.MILLISECONDS ) + .withLeakedSessionsLogging() + .build(); + + Config verify = TestUtil.serializeAndReadBack( config, Config.class ); + + + assertEquals( config.maxConnectionPoolSize(), verify.maxConnectionPoolSize() ); + assertEquals( config.connectionTimeoutMillis(), verify.connectionTimeoutMillis() ); + assertEquals( config.connectionAcquisitionTimeoutMillis(), verify.connectionAcquisitionTimeoutMillis() ); + assertEquals( config.idleTimeBeforeConnectionTest(), verify.idleTimeBeforeConnectionTest() ); + assertEquals( config.maxConnectionLifetimeMillis(), verify.maxConnectionLifetimeMillis() ); + assertNotNull( verify.retrySettings() ); + assertSame( DevNullLogging.DEV_NULL_LOGGING, verify.logging() ); + assertEquals( config.retrySettings().maxRetryTimeMs(), verify.retrySettings().maxRetryTimeMs() ); + assertEquals( config.fetchSize(), verify.fetchSize() ); + assertEquals( config.eventLoopThreads(), verify.eventLoopThreads() ); + assertEquals( config.encrypted(), verify.encrypted() ); + assertEquals( config.trustStrategy().strategy(), verify.trustStrategy().strategy() ); + assertEquals( config.trustStrategy().certFile(), verify.trustStrategy().certFile() ); + assertEquals( config.trustStrategy().isHostnameVerificationEnabled(), verify.trustStrategy().isHostnameVerificationEnabled() ); + assertEquals( config.trustStrategy().revocationStrategy(), verify.trustStrategy().revocationStrategy() ); + assertEquals( config.userAgent(), verify.userAgent() ); + assertEquals( config.isMetricsEnabled(), verify.isMetricsEnabled() ); + assertEquals( config.routingSettings().routingTablePurgeDelayMs(), verify.routingSettings().routingTablePurgeDelayMs() ); + assertEquals( config.logLeakedSessions(), verify.logLeakedSessions() ); + } + + @Test + void shouldSerializeSerializableLogging() throws IOException, ClassNotFoundException + { + + Config config = Config.builder().withLogging( Logging.javaUtilLogging( Level.ALL ) ).build(); + + Config verify = TestUtil.serializeAndReadBack( config, Config.class ); + Logging logging = verify.logging(); + assertInstanceOf( JULogging.class, logging ); + + List loggingLevelFields = + ReflectionSupport.findFields( JULogging.class, f -> "loggingLevel".equals( f.getName() ), HierarchyTraversalMode.TOP_DOWN ); + assertFalse( loggingLevelFields.isEmpty() ); + loggingLevelFields.forEach( field -> + { + try + { + field.setAccessible( true ); + assertEquals( Level.ALL, field.get( logging ) ); + } + catch ( IllegalAccessException e ) + { + throw new RuntimeException( e ); + } + } ); + } + + @ParameterizedTest + @ValueSource( classes = {DevNullLogging.class, JULogging.class, ConsoleLogging.class, Slf4jLogging.class} ) + void officialLoggingProvidersShouldBeSerializable( Class loggingClass ) + { + assertTrue( Serializable.class.isAssignableFrom( loggingClass ) ); + } + } } diff --git a/driver/src/test/java/org/neo4j/driver/SessionConfigTest.java b/driver/src/test/java/org/neo4j/driver/SessionConfigTest.java index 1eb27d8ee4..62bb873bda 100644 --- a/driver/src/test/java/org/neo4j/driver/SessionConfigTest.java +++ b/driver/src/test/java/org/neo4j/driver/SessionConfigTest.java @@ -25,18 +25,21 @@ import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; +import org.neo4j.driver.util.TestUtil; + import static java.util.Collections.emptyList; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.params.provider.Arguments.arguments; @@ -47,7 +50,7 @@ class SessionConfigTest { @Test - void shouldReturnDefaultValues() throws Throwable + void shouldReturnDefaultValues() { SessionConfig config = defaultConfig(); @@ -59,7 +62,7 @@ void shouldReturnDefaultValues() throws Throwable @ParameterizedTest @EnumSource( AccessMode.class ) - void shouldChangeAccessMode( AccessMode mode ) throws Throwable + void shouldChangeAccessMode( AccessMode mode ) { SessionConfig config = builder().withDefaultAccessMode( mode ).build(); assertEquals( mode, config.defaultAccessMode() ); @@ -75,7 +78,7 @@ void shouldChangeDatabaseName( String databaseName ) } @Test - void shouldNotAllowNullDatabaseName() throws Throwable + void shouldNotAllowNullDatabaseName() { assertThrows( NullPointerException.class, () -> builder().withDatabase( null ) ); } @@ -98,14 +101,14 @@ static Stream someConfigs() { @ParameterizedTest @ValueSource( strings = {""} ) - void shouldForbiddenEmptyStringDatabaseName( String databaseName ) throws Throwable + void shouldForbiddenEmptyStringDatabaseName( String databaseName ) { IllegalArgumentException error = assertThrows( IllegalArgumentException.class, () -> builder().withDatabase( databaseName ) ); - assertThat( error.getMessage(), startsWith( "Illegal database name " ) ); + assertTrue( error.getMessage().startsWith( "Illegal database name " ) ); } @Test - void shouldAcceptNullBookmarks() throws Throwable + void shouldAcceptNullBookmarks() { SessionConfig config = builder().withBookmarks( (Bookmark[]) null ).build(); assertNull( config.bookmarks() ); @@ -115,7 +118,7 @@ void shouldAcceptNullBookmarks() throws Throwable } @Test - void shouldAcceptEmptyBookmarks() throws Throwable + void shouldAcceptEmptyBookmarks() { SessionConfig config = builder().withBookmarks().build(); assertEquals( emptyList(), config.bookmarks() ); @@ -125,50 +128,79 @@ void shouldAcceptEmptyBookmarks() throws Throwable } @Test - void shouldAcceptBookmarks() throws Throwable + void shouldAcceptBookmarks() { Bookmark one = parse( "one" ); Bookmark two = parse( "two" ); SessionConfig config = builder().withBookmarks( one, two ).build(); - assertThat( config.bookmarks(), equalTo( Arrays.asList( one, two ) ) ); + assertEquals( Arrays.asList( one, two ), config.bookmarks() ); SessionConfig config2 = builder().withBookmarks( Arrays.asList( one, two ) ).build(); - assertThat( config2.bookmarks(), equalTo( Arrays.asList( one, two ) ) ); + assertEquals( Arrays.asList( one, two ), config2.bookmarks() ); } @Test - void shouldAcceptNullInBookmarks() throws Throwable + void shouldAcceptNullInBookmarks() { Bookmark one = parse( "one" ); Bookmark two = parse( "two" ); SessionConfig config = builder().withBookmarks( one, two, null ).build(); - assertThat( config.bookmarks(), equalTo( Arrays.asList( one, two, null ) ) ); + assertEquals( Arrays.asList( one, two, null ), config.bookmarks() ); SessionConfig config2 = builder().withBookmarks( Arrays.asList( one, two, null ) ).build(); - assertThat( config2.bookmarks(), equalTo( Arrays.asList( one, two, null ) ) ); + assertEquals( Arrays.asList( one, two, null ), config2.bookmarks() ); } @ParameterizedTest @ValueSource( longs = {100, 1, 1000, Long.MAX_VALUE, -1} ) - void shouldChangeFetchSize( long value ) throws Throwable + void shouldChangeFetchSize( long value ) { SessionConfig config = builder().withFetchSize( value ).build(); - assertThat( config.fetchSize(), equalTo( Optional.of( value ) ) ); + assertEquals( Optional.of( value ), config.fetchSize()); } @ParameterizedTest @ValueSource( longs = {0, -100, -2} ) - void shouldErrorWithIllegalFetchSize( long value ) throws Throwable + void shouldErrorWithIllegalFetchSize( long value ) { assertThrows( IllegalArgumentException.class, () -> builder().withFetchSize( value ).build() ); } @Test - void shouldTwoConfigBeEqual() throws Throwable + void shouldTwoConfigBeEqual() { SessionConfig config1 = builder().withFetchSize( 100 ).build(); SessionConfig config2 = builder().withFetchSize( 100 ).build(); assertEquals( config1, config2 ); } + + @Test + void shouldSerialize() throws Exception + { + SessionConfig config = SessionConfig.builder() + .withBookmarks( + Bookmark.from( new HashSet<>( Arrays.asList( "bookmarkA", "bookmarkB" ) ) ), + Bookmark.from( new HashSet<>( Arrays.asList( "bookmarkC", "bookmarkD" ) ) ) ) + .withDefaultAccessMode( AccessMode.WRITE ) + .withFetchSize( 54321L ) + .withDatabase( "testing" ) + .withImpersonatedUser( "impersonator" ) + .build(); + + SessionConfig verify = TestUtil.serializeAndReadBack( config, SessionConfig.class ); + + assertNotNull( verify.bookmarks() ); + + List> bookmarks = new ArrayList<>(); + verify.bookmarks().forEach( b -> bookmarks.add( b.values() ) ); + assertEquals( 2, bookmarks.size() ); + assertTrue( bookmarks.get( 0 ).containsAll( Arrays.asList( "bookmarkA", "bookmarkB" ) ) ); + assertTrue( bookmarks.get( 1 ).containsAll( Arrays.asList( "bookmarkC", "bookmarkD" ) ) ); + + assertEquals( config.defaultAccessMode(), verify.defaultAccessMode() ); + assertEquals( config.fetchSize(), verify.fetchSize() ); + assertEquals( config.database(), verify.database() ); + assertEquals( config.impersonatedUser(), verify.impersonatedUser() ); + } } diff --git a/driver/src/test/java/org/neo4j/driver/TransactionConfigTest.java b/driver/src/test/java/org/neo4j/driver/TransactionConfigTest.java index 8acf6a4f36..31740bdb82 100644 --- a/driver/src/test/java/org/neo4j/driver/TransactionConfigTest.java +++ b/driver/src/test/java/org/neo4j/driver/TransactionConfigTest.java @@ -24,12 +24,11 @@ import java.util.HashMap; import java.util.Map; -import org.neo4j.driver.TransactionConfig; -import org.neo4j.driver.Value; import org.neo4j.driver.internal.InternalNode; import org.neo4j.driver.internal.InternalPath; import org.neo4j.driver.internal.InternalRelationship; import org.neo4j.driver.exceptions.ClientException; +import org.neo4j.driver.util.TestUtil; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; @@ -118,4 +117,38 @@ void shouldHaveMetadata() assertEquals( value( true ), metadata.get( "key2" ) ); assertEquals( value( 42 ), metadata.get( "key3" ) ); } + + @Test + void shouldNotModifyMetadataAfterItIsSuppliedToBuilder() { + + Map metadata = new HashMap<>(); + metadata.put( "key1", "value1" ); + metadata.put( "key2", true ); + metadata.put( "key3", 42 ); + + TransactionConfig.Builder builder = TransactionConfig.builder().withMetadata( metadata ); + metadata.put( "key4", "what?" ); + + TransactionConfig config = builder.build(); + assertEquals( 3, config.metadata().size() ); + } + + @Test + void shouldSerialize() throws Exception + { + Map metadata = new HashMap<>(); + metadata.put( "key1", "value1" ); + metadata.put( "key2", true ); + metadata.put( "key3", 42 ); + + TransactionConfig config = TransactionConfig.builder() + .withTimeout( Duration.ofMillis(12345L) ) + .withMetadata( metadata ) + .build(); + + TransactionConfig verify = TestUtil.serializeAndReadBack( config, TransactionConfig.class ); + + assertEquals(config.timeout(), verify.timeout()); + assertEquals(config.metadata(), verify.metadata()); + } } diff --git a/driver/src/test/java/org/neo4j/driver/util/TestUtil.java b/driver/src/test/java/org/neo4j/driver/util/TestUtil.java index 0df604d59c..7daf34448f 100644 --- a/driver/src/test/java/org/neo4j/driver/util/TestUtil.java +++ b/driver/src/test/java/org/neo4j/driver/util/TestUtil.java @@ -28,6 +28,12 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -780,4 +786,20 @@ private static void releaseIfPossible( ByteBuf buf ) buf.release(); } } + + public static T serializeAndReadBack( T instance, Class targetClass ) throws IOException, ClassNotFoundException + { + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try ( ObjectOutputStream oos = new ObjectOutputStream( bos ) ) + { + oos.writeObject( instance ); + } + bos.close(); + + try ( ObjectInputStream oos = new ObjectInputStream( new ByteArrayInputStream( bos.toByteArray() ) ) ) + { + return targetClass.cast( oos.readObject() ); + } + } }