From 569acfa31dabc3052621c699f0680b338a7332aa Mon Sep 17 00:00:00 2001 From: jruaux Date: Thu, 14 Mar 2024 22:54:23 -0700 Subject: [PATCH] refactor: created separate connector for Redis replication and ping --- build.gradle | 19 +- connectors/riot-db/riot-db.gradle | 4 +- .../com/redis/riot/db/DataSourceOptions.java | 69 ++- .../com/redis/riot/db/DatabaseExport.java | 98 ++-- .../com/redis/riot/db/DatabaseImport.java | 22 +- .../java/com/redis/riot/db/DatabaseUtils.java | 21 - .../com/redis/riot/faker/FakerImport.java | 24 +- .../com/redis/riot/faker/FakerItemReader.java | 19 +- .../redis/riot/faker/FakerReaderTests.java | 51 +- connectors/riot-file/riot-file.gradle | 1 - .../com/redis/riot/file/FileDumpExport.java | 22 +- .../com/redis/riot/file/FileDumpImport.java | 5 +- .../java/com/redis/riot/file/FileImport.java | 17 +- .../java/com/redis/riot/file/FileUtils.java | 479 +++++++++--------- .../redis/riot/file/KeyValueDeserializer.java | 161 ++++++ .../redis/riot/file/MapToFieldFunction.java | 25 + .../com/redis/riot/file}/ToMapFunction.java | 2 +- ...{FileTests.java => AbstractFileTests.java} | 76 ++- .../com/redis/riot/file/JsonSerdeTests.java | 86 ++++ .../com/redis/riot/file/StackFileTests.java | 9 +- connectors/riot-redis/gradle.properties | 19 + connectors/riot-redis/riot-redis.gradle | 49 ++ .../com/redis/riot/redis/CompareMode.java | 5 + .../redis/riot/redis}/GeneratorImport.java | 7 +- .../riot/redis}/KeyComparisonDiffLogger.java | 2 +- .../KeyComparisonStatusCountItemWriter.java | 2 +- .../redis}/KeyComparisonSummaryLogger.java | 2 +- .../riot/redis}/KeyValueWriteListener.java | 3 +- .../main/java/com/redis/riot/redis/Ping.java | 127 +++++ .../com/redis/riot/redis}/Replication.java | 273 +++++----- .../com/redis/riot/redis/ReplicationMode.java | 5 + .../com/redis/riot/redis/ReplicationType.java | 5 + .../riot/redis}/AbstractReplicationTests.java | 22 +- .../riot/redis}/RedisContainerFactory.java | 2 +- .../java/com/redis/riot/redis/StackTests.java | 20 + core/riot-core/riot-core.gradle | 3 - .../com/redis/riot/core/AbstractExport.java | 64 ++- .../com/redis/riot/core/AbstractImport.java | 120 ++--- .../redis/riot/core/AbstractJobRunnable.java | 175 +++---- .../redis/riot/core/AbstractMapExport.java | 16 +- .../redis/riot/core/AbstractRiotRunnable.java | 192 ------- .../com/redis/riot/core/AbstractRunnable.java | 65 +++ .../redis/riot/core/AbstractStructImport.java | 18 +- .../redis/riot/core/BlockedKeyItemWriter.java | 55 -- .../java/com/redis/riot/core/CompareMode.java | 7 - .../riot/core/EvaluationContextOptions.java | 66 +++ .../redis/riot/core/ExecutionException.java | 19 + .../riot/core/ExportProcessorOptions.java | 58 +++ .../riot/core/ImportProcessorOptions.java | 95 ++++ .../com/redis/riot/core/KeyFilterOptions.java | 44 +- .../redis/riot/core/KeyValueDeserializer.java | 168 ------ .../riot/core/KeyValueProcessorOptions.java | 61 --- .../main/java/com/redis/riot/core/Ping.java | 134 ----- ...isOptions.java => RedisClientOptions.java} | 113 ++++- .../com/redis/riot/core/RedisContext.java | 45 -- .../redis/riot/core/RedisReaderOptions.java | 6 +- .../redis/riot/core/RedisWriterOptions.java | 101 ++-- .../redis/riot/core/ReplicationContext.java | 25 - .../com/redis/riot/core/ReplicationMode.java | 5 - .../com/redis/riot/core/ReplicationType.java | 5 - .../java/com/redis/riot/core/RiotContext.java | 29 -- .../com/redis/riot/core/RiotException.java | 19 - .../java/com/redis/riot/core/RiotStep.java | 74 ++- .../redis/riot/core/RuntimeRiotException.java | 23 - .../com/redis/riot/core/StructDiffLogger.java | 51 -- .../redis/riot/core/ThrottledItemWriter.java | 1 - .../core/function/MapToFieldFunction.java | 25 - .../function/MapToStringArrayFunction.java | 23 - .../function/RegexNamedGroupFunction.java | 53 +- .../core/function/SampleToMapFunction.java | 22 - .../core/function/StringToMapFunction.java | 25 +- .../core/function/StructToMapFunction.java | 164 +++--- .../riot/core/function/ZsetToMapFunction.java | 64 ++- .../AbstractFilterMapOperationBuilder.java | 53 +- .../AbstractMapOperationBuilder.java | 6 - .../redis/riot/core/operation/DelBuilder.java | 8 +- .../riot/core/operation/ExpireAtBuilder.java | 3 +- .../riot/core/operation/ExpireBuilder.java | 4 +- .../riot/core/operation/GeoaddBuilder.java | 4 +- .../riot/core/operation/HsetBuilder.java | 4 +- .../riot/core/operation/JsonSetBuilder.java | 3 +- .../riot/core/operation/LpushBuilder.java | 4 +- .../riot/core/operation/RpushBuilder.java | 4 +- .../riot/core/operation/SaddBuilder.java | 4 +- .../redis/riot/core/operation/SetBuilder.java | 6 +- .../riot/core/operation/SugaddBuilder.java | 9 +- .../riot/core/operation/TsAddBuilder.java | 4 +- .../riot/core/operation/XaddSupplier.java | 5 +- .../riot/core/operation/ZaddSupplier.java | 4 +- .../riot/core/{test => }/ConverterTests.java | 2 +- .../riot/core/{test => }/FunctionTests.java | 2 +- .../com/redis/riot/core/ProcessorTests.java | 81 +++ .../redis/riot/core/test/JsonSerdeTests.java | 88 ---- .../redis/riot/core/test/ProcessorTests.java | 96 ---- .../com/redis/riot/core/test/StackTests.java | 28 - docs/guide/src/docs/asciidoc/replication.adoc | 2 +- gradle.properties | 34 +- gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew.bat | 20 +- plugins/riot/riot.gradle | 47 +- .../redis/riot/cli/AbstractExportCommand.java | 6 +- .../redis/riot/cli/AbstractImportCommand.java | 452 ++--------------- .../redis/riot/cli/AbstractJobCommand.java | 26 +- .../redis/riot/cli/AbstractMainCommand.java | 94 ++++ .../cli/AbstractManifestVersionProvider.java | 63 +++ .../redis/riot/cli/AbstractRiotCommand.java | 36 -- .../riot/cli/AbstractStructImportCommand.java | 18 +- .../redis/riot/cli/AbstractSubCommand.java | 23 + .../java/com/redis/riot/cli/BaseCommand.java | 34 +- .../redis/riot/cli/DatabaseExportCommand.java | 14 +- .../redis/riot/cli/DatabaseImportCommand.java | 28 +- .../redis/riot/cli/EvaluationContextArgs.java | 26 + .../redis/riot/cli/FakerImportCommand.java | 14 +- .../redis/riot/cli/FileDumpExportCommand.java | 28 +- .../redis/riot/cli/FileDumpImportCommand.java | 12 +- .../com/redis/riot/cli/FileImportCommand.java | 34 +- .../com/redis/riot/cli/GenerateCommand.java | 34 +- .../redis/riot/cli/KeyValueProcessorArgs.java | 6 +- .../java/com/redis/riot/cli/LoggingMixin.java | 37 +- .../main/java/com/redis/riot/cli/Main.java | 89 +--- .../riot/cli/ManifestVersionProvider.java | 79 +-- .../java/com/redis/riot/cli/PingCommand.java | 58 +-- .../java/com/redis/riot/cli/RedisArgs.java | 10 +- .../java/com/redis/riot/cli/RedisCommand.java | 11 + .../com/redis/riot/cli/RedisReaderArgs.java | 2 +- .../com/redis/riot/cli/ReplicateCommand.java | 22 +- .../redis/AbstractRedisCollectionCommand.java | 29 ++ .../riot/cli/redis/AbstractRedisCommand.java | 44 ++ .../com/redis/riot/cli/redis/DelCommand.java | 15 + .../redis/riot/cli/redis/ExpireCommand.java | 29 ++ .../riot/cli/redis/FieldFilteringArgs.java | 22 + .../redis/riot/cli/redis/GeoaddCommand.java | 25 + .../com/redis/riot/cli/redis/HsetCommand.java | 21 + .../redis/riot/cli/redis/JsonSetCommand.java | 21 + .../redis/riot/cli/redis/LpushCommand.java | 15 + .../redis/riot/cli/redis/RpushCommand.java | 15 + .../com/redis/riot/cli/redis/SaddCommand.java | 15 + .../com/redis/riot/cli/redis/SetCommand.java | 32 ++ .../redis/riot/cli/redis/SugaddCommand.java | 81 +++ .../redis/riot/cli/redis/TsAddCommand.java | 37 ++ .../com/redis/riot/cli/redis/XaddCommand.java | 53 ++ .../com/redis/riot/cli/redis/ZaddCommand.java | 25 + .../com/redis/riot/Messages.properties | 15 - .../com/redis/riot/cli/AbstractDbTests.java | 143 +++--- .../riot/cli/AbstractIntegrationTests.java | 6 +- .../riot/cli/AbstractReplicationTests.java | 9 +- .../redis/riot/cli/AbstractRiotTestBase.java | 78 ++- .../cli/EnterpriseServerToStackTests.java | 8 - .../riot/cli/EnterpriseToStackTests.java | 8 - .../com/redis/riot/cli/PostgresTests.java | 22 +- .../cli/StackToEnterpriseContainerTests.java | 28 +- .../cli/StackToEnterpriseServerTests.java | 8 - .../cli/StackToStackIntegrationTests.java | 28 +- .../cli/StackToStackReplicationTests.java | 8 - plugins/riot/src/test/resources/replicate-hll | 2 +- .../src/test/resources/replicate-live-struct | 2 +- .../riot/src/test/resources/replicate-struct | 2 +- settings.gradle | 2 - 159 files changed, 3244 insertions(+), 3373 deletions(-) delete mode 100644 connectors/riot-db/src/main/java/com/redis/riot/db/DatabaseUtils.java create mode 100644 connectors/riot-file/src/main/java/com/redis/riot/file/KeyValueDeserializer.java create mode 100644 connectors/riot-file/src/main/java/com/redis/riot/file/MapToFieldFunction.java rename {core/riot-core/src/main/java/com/redis/riot/core/function => connectors/riot-file/src/main/java/com/redis/riot/file}/ToMapFunction.java (96%) rename connectors/riot-file/src/test/java/com/redis/riot/file/{FileTests.java => AbstractFileTests.java} (53%) create mode 100644 connectors/riot-file/src/test/java/com/redis/riot/file/JsonSerdeTests.java create mode 100644 connectors/riot-redis/gradle.properties create mode 100644 connectors/riot-redis/riot-redis.gradle create mode 100644 connectors/riot-redis/src/main/java/com/redis/riot/redis/CompareMode.java rename {core/riot-core/src/main/java/com/redis/riot/core => connectors/riot-redis/src/main/java/com/redis/riot/redis}/GeneratorImport.java (97%) rename {core/riot-core/src/main/java/com/redis/riot/core => connectors/riot-redis/src/main/java/com/redis/riot/redis}/KeyComparisonDiffLogger.java (97%) rename {core/riot-core/src/main/java/com/redis/riot/core => connectors/riot-redis/src/main/java/com/redis/riot/redis}/KeyComparisonStatusCountItemWriter.java (98%) rename {core/riot-core/src/main/java/com/redis/riot/core => connectors/riot-redis/src/main/java/com/redis/riot/redis}/KeyComparisonSummaryLogger.java (97%) rename {core/riot-core/src/main/java/com/redis/riot/core => connectors/riot-redis/src/main/java/com/redis/riot/redis}/KeyValueWriteListener.java (97%) create mode 100644 connectors/riot-redis/src/main/java/com/redis/riot/redis/Ping.java rename {core/riot-core/src/main/java/com/redis/riot/core => connectors/riot-redis/src/main/java/com/redis/riot/redis}/Replication.java (61%) create mode 100644 connectors/riot-redis/src/main/java/com/redis/riot/redis/ReplicationMode.java create mode 100644 connectors/riot-redis/src/main/java/com/redis/riot/redis/ReplicationType.java rename {core/riot-core/src/test/java/com/redis/riot/core/test => connectors/riot-redis/src/test/java/com/redis/riot/redis}/AbstractReplicationTests.java (87%) rename {core/riot-core/src/test/java/com/redis/riot/core/test => connectors/riot-redis/src/test/java/com/redis/riot/redis}/RedisContainerFactory.java (95%) create mode 100644 connectors/riot-redis/src/test/java/com/redis/riot/redis/StackTests.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/AbstractRiotRunnable.java create mode 100644 core/riot-core/src/main/java/com/redis/riot/core/AbstractRunnable.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/BlockedKeyItemWriter.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/CompareMode.java create mode 100644 core/riot-core/src/main/java/com/redis/riot/core/EvaluationContextOptions.java create mode 100644 core/riot-core/src/main/java/com/redis/riot/core/ExecutionException.java create mode 100644 core/riot-core/src/main/java/com/redis/riot/core/ExportProcessorOptions.java create mode 100644 core/riot-core/src/main/java/com/redis/riot/core/ImportProcessorOptions.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/KeyValueDeserializer.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/KeyValueProcessorOptions.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/Ping.java rename core/riot-core/src/main/java/com/redis/riot/core/{RedisOptions.java => RedisClientOptions.java} (53%) delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/RedisContext.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/ReplicationContext.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/ReplicationMode.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/ReplicationType.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/RiotContext.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/RiotException.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/RuntimeRiotException.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/StructDiffLogger.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/function/MapToFieldFunction.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/function/MapToStringArrayFunction.java delete mode 100644 core/riot-core/src/main/java/com/redis/riot/core/function/SampleToMapFunction.java rename core/riot-core/src/test/java/com/redis/riot/core/{test => }/ConverterTests.java (98%) rename core/riot-core/src/test/java/com/redis/riot/core/{test => }/FunctionTests.java (97%) create mode 100644 core/riot-core/src/test/java/com/redis/riot/core/ProcessorTests.java delete mode 100644 core/riot-core/src/test/java/com/redis/riot/core/test/JsonSerdeTests.java delete mode 100644 core/riot-core/src/test/java/com/redis/riot/core/test/ProcessorTests.java delete mode 100644 core/riot-core/src/test/java/com/redis/riot/core/test/StackTests.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/AbstractMainCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/AbstractManifestVersionProvider.java delete mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/AbstractRiotCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/AbstractSubCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/EvaluationContextArgs.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/RedisCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/AbstractRedisCollectionCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/AbstractRedisCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/DelCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/ExpireCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/FieldFilteringArgs.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/GeoaddCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/HsetCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/JsonSetCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/LpushCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/RpushCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/SaddCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/SetCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/SugaddCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/TsAddCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/XaddCommand.java create mode 100644 plugins/riot/src/main/java/com/redis/riot/cli/redis/ZaddCommand.java delete mode 100644 plugins/riot/src/main/resources/com/redis/riot/Messages.properties diff --git a/build.gradle b/build.gradle index 5fb7d36b9..b548274b9 100644 --- a/build.gradle +++ b/build.gradle @@ -94,12 +94,12 @@ subprojects { subproj -> } dependencies { - compileOnly group: 'com.google.code.findbugs', name: 'jsr305', version: jsr305Version - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: junitVersion - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junitVersion - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junitVersion - testImplementation group: 'org.junit.platform', name: 'junit-platform-launcher', version: junitPlatformVersion - testImplementation group: 'org.testcontainers', name: 'junit-jupiter', version: testcontainersVersion + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.junit.jupiter:junit-jupiter-params' + testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.slf4j:slf4j-simple' testImplementation group: 'com.redis', name: 'testcontainers-redis', version: testcontainersRedisVersion } @@ -111,13 +111,6 @@ subprojects { subproj -> all*.exclude module: 'spring-boot-starter-logging' } - configurations.all { - resolutionStrategy.eachDependency { DependencyResolveDetails details -> - if (details.requested.name == 'lettuce-core' ) { - details.useVersion lettuceVersion - } - } - } } } diff --git a/connectors/riot-db/riot-db.gradle b/connectors/riot-db/riot-db.gradle index ba3147f87..11a82c1e6 100644 --- a/connectors/riot-db/riot-db.gradle +++ b/connectors/riot-db/riot-db.gradle @@ -27,8 +27,8 @@ dependencies { implementation 'org.springframework:spring-jdbc' implementation 'com.mysql:mysql-connector-j' implementation 'org.postgresql:postgresql' - implementation group: 'com.microsoft.sqlserver', name: 'mssql-jdbc', version: mssqlVersion - implementation group: 'com.oracle.ojdbc', name: 'ojdbc8', version: oracleVersion + implementation 'com.microsoft.sqlserver:mssql-jdbc' + implementation 'com.oracle.database.jdbc:ojdbc11' } compileJava { diff --git a/connectors/riot-db/src/main/java/com/redis/riot/db/DataSourceOptions.java b/connectors/riot-db/src/main/java/com/redis/riot/db/DataSourceOptions.java index fd4cb7e53..c25c73cef 100644 --- a/connectors/riot-db/src/main/java/com/redis/riot/db/DataSourceOptions.java +++ b/connectors/riot-db/src/main/java/com/redis/riot/db/DataSourceOptions.java @@ -1,45 +1,58 @@ package com.redis.riot.db; +import javax.sql.DataSource; + +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; + public class DataSourceOptions { - private String driver; + private String driver; + + private String url; - private String url; + private String username; - private String username; + private String password; - private String password; + public String getDriver() { + return driver; + } - public String getDriver() { - return driver; - } + public void setDriver(String driver) { + this.driver = driver; + } - public void setDriver(String driver) { - this.driver = driver; - } + public String getUrl() { + return url; + } - public String getUrl() { - return url; - } + public void setUrl(String url) { + this.url = url; + } - public void setUrl(String url) { - this.url = url; - } + public String getUsername() { + return username; + } - public String getUsername() { - return username; - } + public void setUsername(String username) { + this.username = username; + } - public void setUsername(String username) { - this.username = username; - } + public String getPassword() { + return password; + } - public String getPassword() { - return password; - } + public void setPassword(String password) { + this.password = password; + } - public void setPassword(String password) { - this.password = password; - } + public DataSource dataSource() { + DataSourceProperties properties = new DataSourceProperties(); + properties.setUrl(url); + properties.setDriverClassName(driver); + properties.setUsername(username); + properties.setPassword(password); + return properties.initializeDataSourceBuilder().build(); + } } diff --git a/connectors/riot-db/src/main/java/com/redis/riot/db/DatabaseExport.java b/connectors/riot-db/src/main/java/com/redis/riot/db/DatabaseExport.java index 4cf28f497..d4db7f9ba 100644 --- a/connectors/riot-db/src/main/java/com/redis/riot/db/DatabaseExport.java +++ b/connectors/riot-db/src/main/java/com/redis/riot/db/DatabaseExport.java @@ -2,8 +2,6 @@ import java.util.Map; -import javax.sql.DataSource; - import org.springframework.batch.item.database.JdbcBatchItemWriter; import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; @@ -13,55 +11,51 @@ public class DatabaseExport extends AbstractMapExport { - public static final boolean DEFAULT_ASSERT_UPDATES = true; - - private String sql; - - private DataSourceOptions dataSourceOptions = new DataSourceOptions(); - - private boolean assertUpdates = DEFAULT_ASSERT_UPDATES; - - public void setSql(String sql) { - this.sql = sql; - } - - public void setDataSourceOptions(DataSourceOptions dataSourceOptions) { - this.dataSourceOptions = dataSourceOptions; - } - - public void setAssertUpdates(boolean assertUpdates) { - this.assertUpdates = assertUpdates; - } - - @Override - protected JdbcBatchItemWriter> writer() { - JdbcBatchItemWriterBuilder> writer = new JdbcBatchItemWriterBuilder<>(); - writer.itemSqlParameterSourceProvider(NullableSqlParameterSource::new); - writer.dataSource(dataSource()); - writer.sql(sql); - writer.assertUpdates(assertUpdates); - return writer.build(); - } - - private DataSource dataSource() { - return DatabaseUtils.dataSource(dataSourceOptions); - } - - private static class NullableSqlParameterSource extends MapSqlParameterSource { - - public NullableSqlParameterSource(@Nullable Map values) { - super(values); - } - - @Override - @Nullable - public Object getValue(String paramName) { - if (!hasValue(paramName)) { - return null; - } - return super.getValue(paramName); - } - - } + public static final boolean DEFAULT_ASSERT_UPDATES = true; + + private String sql; + private DataSourceOptions dataSourceOptions = new DataSourceOptions(); + private boolean assertUpdates = DEFAULT_ASSERT_UPDATES; + + public void setSql(String sql) { + this.sql = sql; + } + + public void setDataSourceOptions(DataSourceOptions dataSourceOptions) { + this.dataSourceOptions = dataSourceOptions; + } + + public void setAssertUpdates(boolean assertUpdates) { + this.assertUpdates = assertUpdates; + } + + @Override + protected JdbcBatchItemWriter> writer() { + JdbcBatchItemWriterBuilder> builder = new JdbcBatchItemWriterBuilder<>(); + builder.itemSqlParameterSourceProvider(NullableSqlParameterSource::new); + builder.dataSource(dataSourceOptions.dataSource()); + builder.sql(sql); + builder.assertUpdates(assertUpdates); + JdbcBatchItemWriter> writer = builder.build(); + writer.afterPropertiesSet(); + return writer; + } + + private static class NullableSqlParameterSource extends MapSqlParameterSource { + + public NullableSqlParameterSource(@Nullable Map values) { + super(values); + } + + @Override + @Nullable + public Object getValue(String paramName) { + if (!hasValue(paramName)) { + return null; + } + return super.getValue(paramName); + } + + } } diff --git a/connectors/riot-db/src/main/java/com/redis/riot/db/DatabaseImport.java b/connectors/riot-db/src/main/java/com/redis/riot/db/DatabaseImport.java index 47dc1cc24..eb1444d8c 100644 --- a/connectors/riot-db/src/main/java/com/redis/riot/db/DatabaseImport.java +++ b/connectors/riot-db/src/main/java/com/redis/riot/db/DatabaseImport.java @@ -2,41 +2,28 @@ import java.util.Map; -import javax.sql.DataSource; - import org.springframework.batch.core.Job; import org.springframework.batch.item.ItemReader; -import org.springframework.batch.item.ItemWriter; import org.springframework.batch.item.database.AbstractCursorItemReader; import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder; import org.springframework.jdbc.core.ColumnMapRowMapper; import org.springframework.util.ClassUtils; import com.redis.riot.core.AbstractImport; -import com.redis.riot.core.RiotContext; public class DatabaseImport extends AbstractImport { public static final int DEFAULT_FETCH_SIZE = AbstractCursorItemReader.VALUE_NOT_SET; - public static final int DEFAULT_MAX_RESULT_SET_ROWS = AbstractCursorItemReader.VALUE_NOT_SET; - public static final int DEFAULT_QUERY_TIMEOUT = AbstractCursorItemReader.VALUE_NOT_SET; private String sql; - private DataSourceOptions dataSourceOptions = new DataSourceOptions(); - private int maxItemCount; - private int fetchSize = DEFAULT_FETCH_SIZE; - private int maxResultSetRows = DEFAULT_MAX_RESULT_SET_ROWS; - private int queryTimeout = DEFAULT_QUERY_TIMEOUT; - private boolean useSharedExtendedConnection; - private boolean verifyCursorPosition; public String getSql() { @@ -104,18 +91,15 @@ public void setVerifyCursorPosition(boolean verifyCursorPosition) { } @Override - protected Job job(RiotContext executionContext) throws Exception { + protected Job job() { String name = ClassUtils.getShortName(getClass()); - ItemReader> reader = reader(); - ItemWriter> writer = writer(executionContext); - return jobBuilder().start(step(name, reader, null, writer).build()).build(); + return jobBuilder().start(step(name, reader(), null, writer()).build()).build(); } private ItemReader> reader() { - DataSource dataSource = DatabaseUtils.dataSource(dataSourceOptions); JdbcCursorItemReaderBuilder> builder = new JdbcCursorItemReaderBuilder<>(); builder.saveState(false); - builder.dataSource(dataSource); + builder.dataSource(dataSourceOptions.dataSource()); builder.name("database-reader"); builder.rowMapper(new ColumnMapRowMapper()); builder.sql(sql); diff --git a/connectors/riot-db/src/main/java/com/redis/riot/db/DatabaseUtils.java b/connectors/riot-db/src/main/java/com/redis/riot/db/DatabaseUtils.java deleted file mode 100644 index c7f4c22cc..000000000 --- a/connectors/riot-db/src/main/java/com/redis/riot/db/DatabaseUtils.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.redis.riot.db; - -import javax.sql.DataSource; - -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; - -public abstract class DatabaseUtils { - - private DatabaseUtils() { - } - - static DataSource dataSource(DataSourceOptions options) { - DataSourceProperties properties = new DataSourceProperties(); - properties.setUrl(options.getUrl()); - properties.setDriverClassName(options.getDriver()); - properties.setUsername(options.getUsername()); - properties.setPassword(options.getPassword()); - return properties.initializeDataSourceBuilder().build(); - } - -} diff --git a/connectors/riot-faker/src/main/java/com/redis/riot/faker/FakerImport.java b/connectors/riot-faker/src/main/java/com/redis/riot/faker/FakerImport.java index 8197fb356..b27e61d0e 100644 --- a/connectors/riot-faker/src/main/java/com/redis/riot/faker/FakerImport.java +++ b/connectors/riot-faker/src/main/java/com/redis/riot/faker/FakerImport.java @@ -15,24 +15,18 @@ import com.redis.lettucemod.search.IndexInfo; import com.redis.lettucemod.util.RedisModulesUtils; import com.redis.riot.core.AbstractImport; -import com.redis.riot.core.RiotContext; import com.redis.riot.core.RiotUtils; import com.redis.spring.batch.common.Range; public class FakerImport extends AbstractImport { public static final int DEFAULT_COUNT = 1000; - public static final Locale DEFAULT_LOCALE = Locale.ENGLISH; - public static final Range DEFAULT_INDEX_RANGE = Range.from(1); private Map fields = new LinkedHashMap<>(); - private int count = DEFAULT_COUNT; - private String searchIndex; - private Locale locale = DEFAULT_LOCALE; public Map getFields() { @@ -69,32 +63,32 @@ public void setLocale(Locale locale) { } @Override - protected Job job(RiotContext executionContext) throws Exception { + protected Job job() { String name = ClassUtils.getShortName(getClass()); - FakerItemReader reader = reader(executionContext); - ItemWriter> writer = writer(executionContext); + FakerItemReader reader = reader(); + ItemWriter> writer = writer(); return jobBuilder().start(step(name, reader, null, writer).build()).build(); } - private FakerItemReader reader(RiotContext executionContext) { + private FakerItemReader reader() { FakerItemReader reader = new FakerItemReader(); reader.setMaxItemCount(count); reader.setLocale(locale); - reader.setFields(fields(executionContext)); + reader.setFields(fields()); return reader; } - private Map fields(RiotContext executionContext) { + private Map fields() { Map allFields = new LinkedHashMap<>(fields); if (searchIndex != null) { - allFields.putAll(searchIndexFields(executionContext)); + allFields.putAll(searchIndexFields()); } return allFields; } - private Map searchIndexFields(RiotContext executionContext) { + private Map searchIndexFields() { Map searchFields = new LinkedHashMap<>(); - RediSearchCommands commands = executionContext.getRedisContext().getConnection().sync(); + RediSearchCommands commands = getRedisConnection().sync(); IndexInfo info = RedisModulesUtils.indexInfo(commands.ftInfo(searchIndex)); for (Field field : info.getFields()) { searchFields.put(field.getName(), RiotUtils.parse(expression(field))); diff --git a/connectors/riot-faker/src/main/java/com/redis/riot/faker/FakerItemReader.java b/connectors/riot-faker/src/main/java/com/redis/riot/faker/FakerItemReader.java index a94ed2618..39b467cb6 100644 --- a/connectors/riot-faker/src/main/java/com/redis/riot/faker/FakerItemReader.java +++ b/connectors/riot-faker/src/main/java/com/redis/riot/faker/FakerItemReader.java @@ -4,6 +4,7 @@ import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; @@ -73,10 +74,21 @@ protected synchronized void doClose() { public class AugmentedFaker extends Faker { + private final AtomicInteger threadCount = new AtomicInteger(); + private final ThreadLocal threadId = ThreadLocal.withInitial(threadCount::incrementAndGet); + public AugmentedFaker(Locale locale) { super(locale); } + public void setThread(int id) { + threadId.set(id); + } + + public void removeThread() { + threadId.remove(); + } + public int getIndex() { return index(); } @@ -85,13 +97,12 @@ public int index() { return getCurrentItemCount(); } - public long getThread() { + public int getThread() { return thread(); } - @SuppressWarnings("deprecation") - public long thread() { - return Thread.currentThread().getId(); + public int thread() { + return threadId.get(); } } diff --git a/connectors/riot-faker/src/test/java/com/redis/riot/faker/FakerReaderTests.java b/connectors/riot-faker/src/test/java/com/redis/riot/faker/FakerReaderTests.java index 20df6040d..d91b61cff 100644 --- a/connectors/riot-faker/src/test/java/com/redis/riot/faker/FakerReaderTests.java +++ b/connectors/riot-faker/src/test/java/com/redis/riot/faker/FakerReaderTests.java @@ -12,33 +12,32 @@ class FakerReaderTests { - public static List readAll(ItemReader reader) throws Exception { - List list = new ArrayList<>(); - T element; - while ((element = reader.read()) != null) { - list.add(element); - } - return list; - } + public static List readAll(ItemReader reader) throws Exception { + List list = new ArrayList<>(); + T element; + while ((element = reader.read()) != null) { + list.add(element); + } + return list; + } - @SuppressWarnings("deprecation") @Test - void fakerReader() throws Exception { - int count = 100; - FakerItemReader reader = new FakerItemReader(); - Map fields = new LinkedHashMap(); - fields.put("index", "index"); - fields.put("firstName", "name.firstName"); - fields.put("lastName", "name.lastName"); - fields.put("thread", "thread"); - reader.setStringFields(fields); - reader.setMaxItemCount(count); - reader.open(new ExecutionContext()); - List> items = readAll(reader); - reader.close(); - Assertions.assertEquals(count, items.size()); - Assertions.assertEquals(1, items.get(0).get("index")); - Assertions.assertEquals(Thread.currentThread().getId(), ((Long) items.get(0).get("thread")).longValue()); - } + void fakerReader() throws Exception { + int count = 100; + FakerItemReader reader = new FakerItemReader(); + Map fields = new LinkedHashMap(); + fields.put("index", "index"); + fields.put("firstName", "name.firstName"); + fields.put("lastName", "name.lastName"); + fields.put("thread", "thread"); + reader.setStringFields(fields); + reader.setMaxItemCount(count); + reader.open(new ExecutionContext()); + List> items = readAll(reader); + reader.close(); + Assertions.assertEquals(count, items.size()); + Assertions.assertEquals(1, items.get(0).get("index")); + Assertions.assertEquals(1, (Integer) items.get(0).get("thread")); + } } diff --git a/connectors/riot-file/riot-file.gradle b/connectors/riot-file/riot-file.gradle index cd2eb04c2..2b93f188d 100644 --- a/connectors/riot-file/riot-file.gradle +++ b/connectors/riot-file/riot-file.gradle @@ -30,7 +30,6 @@ dependencies { exclude group: 'javax.annotation', module: 'javax.annotation-api' } testImplementation group: 'com.redis', name: 'spring-batch-redis', version: springBatchRedisVersion, classifier: 'tests' - testImplementation group: 'commons-io', name: 'commons-io', version: commonsIoVersion } compileJava { diff --git a/connectors/riot-file/src/main/java/com/redis/riot/file/FileDumpExport.java b/connectors/riot-file/src/main/java/com/redis/riot/file/FileDumpExport.java index 79a2e0f70..bc2345227 100644 --- a/connectors/riot-file/src/main/java/com/redis/riot/file/FileDumpExport.java +++ b/connectors/riot-file/src/main/java/com/redis/riot/file/FileDumpExport.java @@ -13,8 +13,7 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.redis.riot.core.AbstractExport; -import com.redis.riot.core.RiotContext; -import com.redis.riot.core.RiotException; +import com.redis.riot.core.ExecutionException; import com.redis.riot.file.resource.JsonResourceItemWriter; import com.redis.riot.file.resource.JsonResourceItemWriterBuilder; import com.redis.riot.file.resource.XmlResourceItemWriter; @@ -27,23 +26,15 @@ public class FileDumpExport extends AbstractExport { public static final String DEFAULT_ELEMENT_NAME = "record"; - public static final String DEFAULT_ROOT_NAME = "root"; - public static final String DEFAULT_LINE_SEPARATOR = System.getProperty("line.separator"); private String file; - private FileOptions fileOptions = new FileOptions(); - private boolean append; - private String rootName = DEFAULT_ROOT_NAME; - private String elementName = DEFAULT_ELEMENT_NAME; - private String lineSeparator = DEFAULT_LINE_SEPARATOR; - private FileDumpType type; public void setFile(String file) { @@ -84,7 +75,7 @@ private ItemWriter> writer() { try { resource = FileUtils.outputResource(file, fileOptions); } catch (IOException e) { - throw new RiotException("Could not open file for writing: " + file, e); + throw new ExecutionException("Could not open file for writing: " + file, e); } if (dumpType(resource) == FileDumpType.XML) { return xmlWriter(resource); @@ -133,13 +124,12 @@ private JsonObjectMarshaller> xmlMarshaller() { } @Override - protected Job job(RiotContext context) throws Exception { - StructItemReader reader = new StructItemReader<>(context.getRedisContext().getClient(), - StringCodec.UTF8); - configureReader("export-reader", reader, context.getRedisContext()); + protected Job job() { + StructItemReader reader = new StructItemReader<>(getRedisClient(), StringCodec.UTF8); + configureReader("export-reader", reader); ItemWriter> writer = writer(); ItemProcessor, KeyValue> processor = new FunctionItemProcessor<>( - processor(StringCodec.UTF8, context)); + processor(StringCodec.UTF8)); return jobBuilder().start(step(getName(), reader, processor, writer).build()).build(); } diff --git a/connectors/riot-file/src/main/java/com/redis/riot/file/FileDumpImport.java b/connectors/riot-file/src/main/java/com/redis/riot/file/FileDumpImport.java index f16c1f3ad..6045c3dd9 100644 --- a/connectors/riot-file/src/main/java/com/redis/riot/file/FileDumpImport.java +++ b/connectors/riot-file/src/main/java/com/redis/riot/file/FileDumpImport.java @@ -12,7 +12,6 @@ import org.springframework.core.io.Resource; import com.redis.riot.core.AbstractStructImport; -import com.redis.riot.core.RiotContext; import com.redis.spring.batch.RedisItemWriter; import com.redis.spring.batch.common.KeyValue; @@ -41,7 +40,7 @@ public void setType(FileDumpType type) { } @Override - protected Job job(RiotContext executionContext) throws Exception { + protected Job job() { List resources = FileUtils.inputResources(files, fileOptions); if (resources.isEmpty()) { throw new IllegalArgumentException("No file found"); @@ -49,7 +48,7 @@ protected Job job(RiotContext executionContext) throws Exception { List steps = new ArrayList<>(); for (Resource resource : resources) { ItemReader> reader = reader(resource); - RedisItemWriter> writer = writer(executionContext); + RedisItemWriter> writer = writer(); steps.add(step(resource.getFilename(), reader, null, writer).build()); } Iterator iterator = steps.iterator(); diff --git a/connectors/riot-file/src/main/java/com/redis/riot/file/FileImport.java b/connectors/riot-file/src/main/java/com/redis/riot/file/FileImport.java index aa3c1ce85..2f96e0781 100644 --- a/connectors/riot-file/src/main/java/com/redis/riot/file/FileImport.java +++ b/connectors/riot-file/src/main/java/com/redis/riot/file/FileImport.java @@ -36,11 +36,8 @@ import org.springframework.util.ObjectUtils; import com.redis.riot.core.AbstractImport; -import com.redis.riot.core.RiotContext; import com.redis.riot.core.RiotUtils; -import com.redis.riot.core.function.MapToFieldFunction; import com.redis.riot.core.function.RegexNamedGroupFunction; -import com.redis.riot.core.function.ToMapFunction; public class FileImport extends AbstractImport { @@ -125,14 +122,14 @@ public void setContinuationString(String continuationString) { } @Override - protected Job job(RiotContext context) throws Exception { + protected Job job() { List resources = FileUtils.inputResources(files, fileOptions); if (resources.isEmpty()) { throw new IllegalArgumentException("No file found"); } List steps = new ArrayList<>(); for (Resource resource : resources) { - steps.add(step(context, resource)); + steps.add(step(resource)); } Iterator iterator = steps.iterator(); SimpleJobBuilder job = jobBuilder().start(iterator.next()); @@ -142,13 +139,13 @@ protected Job job(RiotContext context) throws Exception { return job.build(); } - private TaskletStep step(RiotContext context, Resource resource) throws Exception { + private TaskletStep step(Resource resource) { ItemReader> reader = reader(resource); if (maxItemCount != null && reader instanceof AbstractItemCountingItemStreamItemReader) { ((AbstractItemCountingItemStreamItemReader>) reader).setMaxItemCount(maxItemCount); } - ItemProcessor, Map> processor = processor(context); - ItemWriter> writer = writer(context); + ItemProcessor, Map> processor = processor(); + ItemWriter> writer = writer(); FaultTolerantStepBuilder, Map> step = step(resource.getFilename(), reader, processor, writer); step.skip(ParseException.class); @@ -289,8 +286,8 @@ public ItemReader> reader(String file) throws IOException { } @Override - protected ItemProcessor, Map> processor(RiotContext executionContext) { - ItemProcessor, Map> processor = super.processor(executionContext); + protected ItemProcessor, Map> processor() { + ItemProcessor, Map> processor = super.processor(); if (CollectionUtils.isEmpty(regexes)) { return processor; } diff --git a/connectors/riot-file/src/main/java/com/redis/riot/file/FileUtils.java b/connectors/riot-file/src/main/java/com/redis/riot/file/FileUtils.java index 44451b9e9..42ff2e692 100644 --- a/connectors/riot-file/src/main/java/com/redis/riot/file/FileUtils.java +++ b/connectors/riot-file/src/main/java/com/redis/riot/file/FileUtils.java @@ -45,8 +45,6 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.ServiceOptions; import com.google.cloud.storage.StorageOptions; -import com.redis.riot.core.KeyValueDeserializer; -import com.redis.riot.core.RuntimeRiotException; import com.redis.riot.file.resource.FilenameInputStreamResource; import com.redis.riot.file.resource.OutputStreamResource; import com.redis.riot.file.resource.UncustomizedUrlResource; @@ -57,243 +55,244 @@ public abstract class FileUtils { - public static final String GS_URI_PREFIX = "gs://"; - - public static final String S3_URI_PREFIX = "s3://"; - - public static final Pattern EXTENSION_PATTERN = Pattern.compile("(?i)\\.(?\\w+)(?:\\.(?gz))?$"); - - private FileUtils() { - } - - /** - * - * @param filename Filename that might include a glob pattern - * @return List of file - * @throws IOException - */ - public static Stream expand(String filename) throws IOException { - if (isFile(filename)) { - return expand(Paths.get(filename)).stream().map(Path::toString); - } - return Stream.of(filename); - } - - public static List expand(Path path) throws IOException { - if (Files.exists(path) || path.getParent() == null || !Files.exists(path.getParent())) { - return Arrays.asList(path); - } - // Path might be glob pattern - try (DirectoryStream stream = Files.newDirectoryStream(path.getParent(), path.getFileName().toString())) { - List paths = new ArrayList<>(); - stream.iterator().forEachRemaining(paths::add); - return paths; - } - } - - public static Stream expandAll(Iterable files) { - return StreamSupport.stream(files.spliterator(), false).flatMap(f -> { - try { - return FileUtils.expand(f); - } catch (IOException e) { - throw new RuntimeRiotException(e); - } - }); - } - - public static boolean isGzip(String file) { - return extensionGroup(file, "gz") != null; - } - - public static FileExtension extension(Resource resource) { - String extension = extensionGroup(resource.getFilename(), "extension"); - if (extension == null) { - return null; - } - return FileExtension.valueOf(extension.toUpperCase()); - } - - private static String extensionGroup(String file, String group) { - Matcher matcher = EXTENSION_PATTERN.matcher(file); - if (matcher.find()) { - return matcher.group(group); - } - return null; - } - - public static boolean isFile(String file) { - return !(isGoogleStorageResource(file) || isAmazonS3Resource(file) || ResourceUtils.isUrl(file) || isConsole(file)); - } - - public static boolean isConsole(String file) { - return "-".equals(file); - } - - public static boolean isAmazonS3Resource(String file) { - return file.startsWith(S3_URI_PREFIX); - } - - public static boolean isGoogleStorageResource(String file) { - return file.startsWith(GS_URI_PREFIX); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - public static JsonItemReader jsonReader(Resource resource, Class type) { - JsonItemReaderBuilder builder = new JsonItemReaderBuilder<>(); - builder.name(resource.getFilename() + "-json-file-reader"); - builder.resource(resource); - JacksonJsonObjectReader jsonReader = new JacksonJsonObjectReader(type); - jsonReader.setMapper(objectMapper()); - builder.jsonObjectReader(jsonReader); - return builder.build(); - } - - public static ObjectMapper objectMapper() { - ObjectMapper mapper = new ObjectMapper(); - configureMapper(mapper); - return mapper; - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static XmlItemReader xmlReader(Resource resource, Class type) { - XmlItemReaderBuilder builder = new XmlItemReaderBuilder<>(); - builder.name(resource.getFilename() + "-xml-file-reader"); - builder.resource(resource); - XmlObjectReader xmlReader = new XmlObjectReader(type); - xmlReader.setMapper(xmlMapper()); - builder.xmlObjectReader(xmlReader); - return builder.build(); - } - - public static XmlMapper xmlMapper() { - XmlMapper mapper = new XmlMapper(); - configureMapper(mapper); - return mapper; - } - - private static void configureMapper(ObjectMapper mapper) { - mapper.configure(DeserializationFeature.USE_LONG_FOR_INTS, true); - SimpleModule module = new SimpleModule(); - module.addDeserializer(KeyValue.class, new KeyValueDeserializer()); - mapper.registerModule(module); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - } - - public static FileDumpType dumpType(Resource resource) { - FileExtension extension = extension(resource); - if (extension == FileExtension.XML) { - return FileDumpType.XML; - } - if (extension == FileExtension.JSON) { - return FileDumpType.JSON; - } - throw new UnsupportedOperationException("Unsupported file extension: " + extension); - } - - public static Resource safeInputResource(String file, FileOptions options) { - try { - return inputResource(file, options); - } catch (IOException e) { - throw new RuntimeRiotException("Could not open file " + file, e); - } - } - - public static Resource inputResource(String file, FileOptions options) throws IOException { - if (FileUtils.isConsole(file)) { - return new FilenameInputStreamResource(System.in, "stdin", "Standard Input"); - } - Resource resource; - try { - resource = resource(file, true, options); - } catch (IOException e) { - throw new RuntimeRiotException("Could not read file " + file, e); - } - resource.getInputStream(); - if (options.isGzipped() || FileUtils.isGzip(file)) { - GZIPInputStream gzipInputStream; - try { - gzipInputStream = new GZIPInputStream(resource.getInputStream()); - } catch (IOException e) { - throw new RuntimeRiotException("Could not open input stream", e); - } - return new FilenameInputStreamResource(gzipInputStream, resource.getFilename(), resource.getDescription()); - } - return resource; - } - - public static WritableResource outputResource(String file, FileOptions options) throws IOException { - if (FileUtils.isConsole(file)) { - return new OutputStreamResource(System.out, "stdout", "Standard Output"); - } - Resource resource = resource(file, false, options); - Assert.notNull(resource, "Could not resolve file " + file); - Assert.isInstanceOf(WritableResource.class, resource); - WritableResource writableResource = (WritableResource) resource; - if (options.isGzipped() || isGzip(file)) { - OutputStream outputStream = writableResource.getOutputStream(); - return new OutputStreamResource(new GZIPOutputStream(outputStream), resource.getFilename(), - resource.getDescription()); - } - return writableResource; - } - - private static GoogleStorageResource googleStorageResource(String location, boolean readOnly, GoogleStorageOptions options) - throws IOException { - StorageOptions.Builder builder = StorageOptions.newBuilder().setProjectId(ServiceOptions.getDefaultProjectId()) - .setHeaderProvider(new UserAgentHeaderProvider(GcpStorageAutoConfiguration.class)); - if (options.getKeyFile() != null) { - builder.setCredentials(GoogleCredentials.fromStream(Files.newInputStream(options.getKeyFile().toPath())) - .createScoped(gcpScope(readOnly).getUrl())); - } - if (options.getEncodedKey() != null) { - builder.setCredentials(GoogleCredentials - .fromStream(new ByteArrayInputStream(Base64.getDecoder().decode(options.getEncodedKey())))); - } - if (options.getProjectId() != null) { - builder.setProjectId(options.getProjectId()); - } - return new GoogleStorageResource(builder.build().getService(), location); - } - - private static GcpScope gcpScope(boolean readOnly) { - if (readOnly) { - return GcpScope.STORAGE_READ_ONLY; - } - return GcpScope.STORAGE_READ_WRITE; - } - - private static Resource resource(String location, boolean readOnly, FileOptions options) throws IOException { - if (FileUtils.isAmazonS3Resource(location)) { - return amazonS3Resource(location, options.getAmazonS3Options()); - } - if (FileUtils.isGoogleStorageResource(location)) { - return googleStorageResource(location, readOnly, options.getGoogleStorageOptions()); - } - if (ResourceUtils.isUrl(location)) { - return new UncustomizedUrlResource(location); - } - return new FileSystemResource(location); - } - - private static Resource amazonS3Resource(String location, AmazonS3Options options) { - AmazonS3ClientBuilder clientBuilder = AmazonS3Client.builder(); - if (options.getRegion() != null) { - clientBuilder.withRegion(options.getRegion()); - } - if (options.getAccessKey() != null) { - if (options.getSecretKey() == null) { - throw new IllegalArgumentException("Amazon S3 secret key not specified"); - } - BasicAWSCredentials credentials = new BasicAWSCredentials(options.getAccessKey(), options.getSecretKey()); - clientBuilder.withCredentials(new AWSStaticCredentialsProvider(credentials)); - } - AmazonS3ProtocolResolver resolver = new AmazonS3ProtocolResolver(clientBuilder); - resolver.afterPropertiesSet(); - return resolver.resolve(location, new DefaultResourceLoader()); - } - - public static List inputResources(List files, FileOptions fileOptions) { - return expandAll(files).map(f -> safeInputResource(f, fileOptions)).collect(Collectors.toList()); - } + public static final String GS_URI_PREFIX = "gs://"; + + public static final String S3_URI_PREFIX = "s3://"; + + public static final Pattern EXTENSION_PATTERN = Pattern.compile("(?i)\\.(?\\w+)(?:\\.(?gz))?$"); + + private FileUtils() { + } + + /** + * + * @param filename Filename that might include a glob pattern + * @return List of file + * @throws IOException + */ + public static Stream expand(String filename) throws IOException { + if (isFile(filename)) { + return expand(Paths.get(filename)).stream().map(Path::toString); + } + return Stream.of(filename); + } + + public static List expand(Path path) throws IOException { + if (Files.exists(path) || path.getParent() == null || !Files.exists(path.getParent())) { + return Arrays.asList(path); + } + // Path might be glob pattern + try (DirectoryStream stream = Files.newDirectoryStream(path.getParent(), path.getFileName().toString())) { + List paths = new ArrayList<>(); + stream.iterator().forEachRemaining(paths::add); + return paths; + } + } + + public static Stream expandAll(Iterable files) { + return StreamSupport.stream(files.spliterator(), false).flatMap(f -> { + try { + return FileUtils.expand(f); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + public static boolean isGzip(String file) { + return extensionGroup(file, "gz") != null; + } + + public static FileExtension extension(Resource resource) { + String extension = extensionGroup(resource.getFilename(), "extension"); + if (extension == null) { + return null; + } + return FileExtension.valueOf(extension.toUpperCase()); + } + + private static String extensionGroup(String file, String group) { + Matcher matcher = EXTENSION_PATTERN.matcher(file); + if (matcher.find()) { + return matcher.group(group); + } + return null; + } + + public static boolean isFile(String file) { + return !(isGoogleStorageResource(file) || isAmazonS3Resource(file) || ResourceUtils.isUrl(file) + || isConsole(file)); + } + + public static boolean isConsole(String file) { + return "-".equals(file); + } + + public static boolean isAmazonS3Resource(String file) { + return file.startsWith(S3_URI_PREFIX); + } + + public static boolean isGoogleStorageResource(String file) { + return file.startsWith(GS_URI_PREFIX); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static JsonItemReader jsonReader(Resource resource, Class type) { + JsonItemReaderBuilder builder = new JsonItemReaderBuilder<>(); + builder.name(resource.getFilename() + "-json-file-reader"); + builder.resource(resource); + JacksonJsonObjectReader jsonReader = new JacksonJsonObjectReader(type); + jsonReader.setMapper(objectMapper()); + builder.jsonObjectReader(jsonReader); + return builder.build(); + } + + public static ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + configureMapper(mapper); + return mapper; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static XmlItemReader xmlReader(Resource resource, Class type) { + XmlItemReaderBuilder builder = new XmlItemReaderBuilder<>(); + builder.name(resource.getFilename() + "-xml-file-reader"); + builder.resource(resource); + XmlObjectReader xmlReader = new XmlObjectReader(type); + xmlReader.setMapper(xmlMapper()); + builder.xmlObjectReader(xmlReader); + return builder.build(); + } + + public static XmlMapper xmlMapper() { + XmlMapper mapper = new XmlMapper(); + configureMapper(mapper); + return mapper; + } + + private static void configureMapper(ObjectMapper mapper) { + mapper.configure(DeserializationFeature.USE_LONG_FOR_INTS, true); + SimpleModule module = new SimpleModule(); + module.addDeserializer(KeyValue.class, new KeyValueDeserializer()); + mapper.registerModule(module); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + public static FileDumpType dumpType(Resource resource) { + FileExtension extension = extension(resource); + if (extension == FileExtension.XML) { + return FileDumpType.XML; + } + if (extension == FileExtension.JSON) { + return FileDumpType.JSON; + } + throw new UnsupportedOperationException("Unsupported file extension: " + extension); + } + + public static Resource safeInputResource(String file, FileOptions options) { + try { + return inputResource(file, options); + } catch (IOException e) { + throw new RuntimeException("Could not open file " + file, e); + } + } + + public static Resource inputResource(String file, FileOptions options) throws IOException { + if (FileUtils.isConsole(file)) { + return new FilenameInputStreamResource(System.in, "stdin", "Standard Input"); + } + Resource resource; + try { + resource = resource(file, true, options); + } catch (IOException e) { + throw new RuntimeException("Could not read file " + file, e); + } + resource.getInputStream(); + if (options.isGzipped() || FileUtils.isGzip(file)) { + GZIPInputStream gzipInputStream; + try { + gzipInputStream = new GZIPInputStream(resource.getInputStream()); + } catch (IOException e) { + throw new RuntimeException("Could not open input stream", e); + } + return new FilenameInputStreamResource(gzipInputStream, resource.getFilename(), resource.getDescription()); + } + return resource; + } + + public static WritableResource outputResource(String file, FileOptions options) throws IOException { + if (FileUtils.isConsole(file)) { + return new OutputStreamResource(System.out, "stdout", "Standard Output"); + } + Resource resource = resource(file, false, options); + Assert.notNull(resource, "Could not resolve file " + file); + Assert.isInstanceOf(WritableResource.class, resource); + WritableResource writableResource = (WritableResource) resource; + if (options.isGzipped() || isGzip(file)) { + OutputStream outputStream = writableResource.getOutputStream(); + return new OutputStreamResource(new GZIPOutputStream(outputStream), resource.getFilename(), + resource.getDescription()); + } + return writableResource; + } + + private static GoogleStorageResource googleStorageResource(String location, boolean readOnly, + GoogleStorageOptions options) throws IOException { + StorageOptions.Builder builder = StorageOptions.newBuilder().setProjectId(ServiceOptions.getDefaultProjectId()) + .setHeaderProvider(new UserAgentHeaderProvider(GcpStorageAutoConfiguration.class)); + if (options.getKeyFile() != null) { + builder.setCredentials(GoogleCredentials.fromStream(Files.newInputStream(options.getKeyFile().toPath())) + .createScoped(gcpScope(readOnly).getUrl())); + } + if (options.getEncodedKey() != null) { + builder.setCredentials(GoogleCredentials + .fromStream(new ByteArrayInputStream(Base64.getDecoder().decode(options.getEncodedKey())))); + } + if (options.getProjectId() != null) { + builder.setProjectId(options.getProjectId()); + } + return new GoogleStorageResource(builder.build().getService(), location); + } + + private static GcpScope gcpScope(boolean readOnly) { + if (readOnly) { + return GcpScope.STORAGE_READ_ONLY; + } + return GcpScope.STORAGE_READ_WRITE; + } + + private static Resource resource(String location, boolean readOnly, FileOptions options) throws IOException { + if (FileUtils.isAmazonS3Resource(location)) { + return amazonS3Resource(location, options.getAmazonS3Options()); + } + if (FileUtils.isGoogleStorageResource(location)) { + return googleStorageResource(location, readOnly, options.getGoogleStorageOptions()); + } + if (ResourceUtils.isUrl(location)) { + return new UncustomizedUrlResource(location); + } + return new FileSystemResource(location); + } + + private static Resource amazonS3Resource(String location, AmazonS3Options options) { + AmazonS3ClientBuilder clientBuilder = AmazonS3Client.builder(); + if (options.getRegion() != null) { + clientBuilder.withRegion(options.getRegion()); + } + if (options.getAccessKey() != null) { + if (options.getSecretKey() == null) { + throw new IllegalArgumentException("Amazon S3 secret key not specified"); + } + BasicAWSCredentials credentials = new BasicAWSCredentials(options.getAccessKey(), options.getSecretKey()); + clientBuilder.withCredentials(new AWSStaticCredentialsProvider(credentials)); + } + AmazonS3ProtocolResolver resolver = new AmazonS3ProtocolResolver(clientBuilder); + resolver.afterPropertiesSet(); + return resolver.resolve(location, new DefaultResourceLoader()); + } + + public static List inputResources(List files, FileOptions fileOptions) { + return expandAll(files).map(f -> safeInputResource(f, fileOptions)).collect(Collectors.toList()); + } } diff --git a/connectors/riot-file/src/main/java/com/redis/riot/file/KeyValueDeserializer.java b/connectors/riot-file/src/main/java/com/redis/riot/file/KeyValueDeserializer.java new file mode 100644 index 000000000..ad096ac48 --- /dev/null +++ b/connectors/riot-file/src/main/java/com/redis/riot/file/KeyValueDeserializer.java @@ -0,0 +1,161 @@ +package com.redis.riot.file; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.LongNode; +import com.redis.lettucemod.timeseries.Sample; +import com.redis.spring.batch.common.DataType; +import com.redis.spring.batch.common.KeyValue; + +import io.lettuce.core.ScoredValue; +import io.lettuce.core.StreamMessage; + +public class KeyValueDeserializer extends StdDeserializer> { + + private static final long serialVersionUID = 1L; + + public static final String KEY = "key"; + public static final String TYPE = "type"; + public static final String VALUE = "value"; + public static final String SCORE = "score"; + public static final String TTL = "ttl"; + public static final String MEMORY_USAGE = "memoryUsage"; + public static final String STREAM = "stream"; + public static final String ID = "id"; + public static final String BODY = "body"; + public static final String TIMESTAMP = "timestamp"; + + public KeyValueDeserializer() { + this(null); + } + + public KeyValueDeserializer(Class> t) { + super(t); + } + + @Override + public KeyValue deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JacksonException { + JsonNode node = p.getCodec().readTree(p); + KeyValue keyValue = new KeyValue<>(); + JsonNode keyNode = node.get(KEY); + if (keyNode != null) { + keyValue.setKey(node.get(KEY).asText()); + } + JsonNode typeNode = node.get(TYPE); + if (typeNode != null) { + String typeString = typeNode.asText(); + if (StringUtils.hasLength(typeString)) { + keyValue.setType(DataType.valueOf(typeString.toUpperCase())); + } + } + LongNode ttlNode = (LongNode) node.get(TTL); + if (ttlNode != null) { + keyValue.setTtl(ttlNode.asLong()); + } + LongNode memUsageNode = (LongNode) node.get(MEMORY_USAGE); + if (memUsageNode != null) { + keyValue.setMemoryUsage(memUsageNode.asLong()); + } + keyValue.setValue(value(keyValue.getType(), node.get(VALUE), ctxt)); + return keyValue; + } + + private Object value(DataType type, JsonNode node, DeserializationContext ctxt) throws IOException { + switch (type) { + case STREAM: + return streamMessages((ArrayNode) node, ctxt); + case ZSET: + return scoredValues((ArrayNode) node); + case TIMESERIES: + return samples((ArrayNode) node); + case HASH: + return ctxt.readTreeAsValue(node, Map.class); + case STRING: + case JSON: + return node.asText(); + case LIST: + return ctxt.readTreeAsValue(node, Collection.class); + case SET: + return ctxt.readTreeAsValue(node, Set.class); + default: + return null; + } + } + + private Collection samples(ArrayNode node) { + Collection samples = new ArrayList<>(node.size()); + for (int index = 0; index < node.size(); index++) { + JsonNode sampleNode = node.get(index); + if (sampleNode != null) { + samples.add(sample(sampleNode)); + } + } + return samples; + } + + private Sample sample(JsonNode node) { + LongNode timestampNode = (LongNode) node.get(TIMESTAMP); + long timestamp = timestampNode == null || timestampNode.isNull() ? 0 : timestampNode.asLong(); + DoubleNode valueNode = (DoubleNode) node.get(VALUE); + double value = valueNode == null || valueNode.isNull() ? 0 : valueNode.asDouble(); + return Sample.of(timestamp, value); + } + + private Collection> scoredValues(ArrayNode node) { + Collection> scoredValues = new ArrayList<>(node.size()); + for (int index = 0; index < node.size(); index++) { + JsonNode scoredValueNode = node.get(index); + if (scoredValueNode != null) { + scoredValues.add(scoredValue(scoredValueNode)); + } + } + return scoredValues; + } + + private ScoredValue scoredValue(JsonNode scoredValueNode) { + JsonNode valueNode = scoredValueNode.get(VALUE); + String value = valueNode == null || valueNode.isNull() ? null : valueNode.asText(); + DoubleNode scoreNode = (DoubleNode) scoredValueNode.get(SCORE); + double score = scoreNode == null || scoreNode.isNull() ? 0 : scoreNode.asDouble(); + return ScoredValue.just(score, value); + } + + private Collection> streamMessages(ArrayNode node, DeserializationContext ctxt) + throws IOException { + Collection> messages = new ArrayList<>(node.size()); + for (int index = 0; index < node.size(); index++) { + JsonNode messageNode = node.get(index); + if (messageNode != null) { + messages.add(streamMessage(messageNode, ctxt)); + } + } + return messages; + } + + @SuppressWarnings("unchecked") + private StreamMessage streamMessage(JsonNode messageNode, DeserializationContext ctxt) + throws IOException { + JsonNode streamNode = messageNode.get(STREAM); + String stream = streamNode == null || streamNode.isNull() ? null : streamNode.asText(); + JsonNode bodyNode = messageNode.get(BODY); + Map body = ctxt.readTreeAsValue(bodyNode, Map.class); + JsonNode idNode = messageNode.get(ID); + String id = idNode == null || idNode.isNull() ? null : idNode.asText(); + return new StreamMessage<>(stream, id, body); + } + +} diff --git a/connectors/riot-file/src/main/java/com/redis/riot/file/MapToFieldFunction.java b/connectors/riot-file/src/main/java/com/redis/riot/file/MapToFieldFunction.java new file mode 100644 index 000000000..77c514636 --- /dev/null +++ b/connectors/riot-file/src/main/java/com/redis/riot/file/MapToFieldFunction.java @@ -0,0 +1,25 @@ +package com.redis.riot.file; + +import java.util.Map; +import java.util.function.Function; + +public class MapToFieldFunction implements Function, Object> { + + private final String key; + + private Object defaultValue = null; + + public MapToFieldFunction(String key) { + this.key = key; + } + + public void setDefaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public Object apply(Map t) { + return t.getOrDefault(key, defaultValue); + } + +} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/function/ToMapFunction.java b/connectors/riot-file/src/main/java/com/redis/riot/file/ToMapFunction.java similarity index 96% rename from core/riot-core/src/main/java/com/redis/riot/core/function/ToMapFunction.java rename to connectors/riot-file/src/main/java/com/redis/riot/file/ToMapFunction.java index 33f1f668e..6bc30b529 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/function/ToMapFunction.java +++ b/connectors/riot-file/src/main/java/com/redis/riot/file/ToMapFunction.java @@ -1,4 +1,4 @@ -package com.redis.riot.core.function; +package com.redis.riot.file; import java.util.Arrays; import java.util.Iterator; diff --git a/connectors/riot-file/src/test/java/com/redis/riot/file/FileTests.java b/connectors/riot-file/src/test/java/com/redis/riot/file/AbstractFileTests.java similarity index 53% rename from connectors/riot-file/src/test/java/com/redis/riot/file/FileTests.java rename to connectors/riot-file/src/test/java/com/redis/riot/file/AbstractFileTests.java index 013f1c69d..30ce6ad0d 100644 --- a/connectors/riot-file/src/test/java/com/redis/riot/file/FileTests.java +++ b/connectors/riot-file/src/test/java/com/redis/riot/file/AbstractFileTests.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -11,46 +12,45 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.springframework.batch.item.NonTransientResourceException; -import org.springframework.batch.item.ParseException; -import org.springframework.batch.item.UnexpectedInputException; +import org.junit.jupiter.api.TestInfo; -import com.redis.riot.core.RedisOptions; +import com.amazonaws.util.IOUtils; +import com.redis.riot.core.RedisClientOptions; import com.redis.riot.core.operation.HsetBuilder; import com.redis.spring.batch.test.AbstractTestBase; -abstract class FileTests extends AbstractTestBase { +abstract class AbstractFileTests extends AbstractTestBase { public static final String BEERS_JSON_URL = "https://storage.googleapis.com/jrx/beers.json"; private static final String ID = "id"; - - private static final String keyspace = "beer"; + private static final String KEYSPACE = "beer"; @SuppressWarnings("unchecked") @Test - void fileImportJSON() throws UnexpectedInputException, ParseException, NonTransientResourceException, Exception { + void fileImportJSON(TestInfo info) throws Exception { FileImport executable = new FileImport(); - executable.setRedisOptions(redisClientOptions()); + executable.setRedisClientOptions(redisClientOptions()); executable.setFiles(BEERS_JSON_URL); HsetBuilder hsetBuilder = new HsetBuilder(); - hsetBuilder.setKeyspace(keyspace); + hsetBuilder.setKeyspace(KEYSPACE); hsetBuilder.setKeyFields(ID); executable.setOperations(hsetBuilder.build()); + executable.setName(name(info)); executable.run(); List keys = commands.keys("*"); assertEquals(216, keys.size()); for (String key : keys) { Map map = commands.hgetall(key); String id = map.get(ID); - assertEquals(key, keyspace + ":" + id); + assertEquals(key, KEYSPACE + ":" + id); } - Map beer1 = commands.hgetall(keyspace + ":1"); + Map beer1 = commands.hgetall(KEYSPACE + ":1"); Assertions.assertEquals("Hocus Pocus", beer1.get("name")); } - private RedisOptions redisClientOptions() { - RedisOptions options = new RedisOptions(); + private RedisClientOptions redisClientOptions() { + RedisClientOptions options = new RedisClientOptions(); options.setCluster(getRedisServer().isRedisCluster()); options.setUri(getRedisServer().getRedisURI()); return options; @@ -58,13 +58,14 @@ private RedisOptions redisClientOptions() { @SuppressWarnings("unchecked") @Test - void fileApiImportCSV() throws UnexpectedInputException, ParseException, NonTransientResourceException, Exception { + void fileApiImportCSV(TestInfo info) throws Exception { FileImport executable = new FileImport(); - executable.setRedisOptions(redisClientOptions()); + executable.setRedisClientOptions(redisClientOptions()); executable.setFiles("https://storage.googleapis.com/jrx/beers.csv"); executable.setHeader(true); + executable.setName(name(info)); HsetBuilder hsetBuilder = new HsetBuilder(); - hsetBuilder.setKeyspace(keyspace); + hsetBuilder.setKeyspace(KEYSPACE); hsetBuilder.setKeyFields(ID); executable.setOperations(hsetBuilder.build()); executable.run(); @@ -73,27 +74,25 @@ void fileApiImportCSV() throws UnexpectedInputException, ParseException, NonTran for (String key : keys) { Map map = commands.hgetall(key); String id = map.get(ID); - assertEquals(key, keyspace + ":" + id); + assertEquals(key, KEYSPACE + ":" + id); } - } @SuppressWarnings("unchecked") @Test - void fileApiFileExpansion() throws IOException { + void fileApiFileExpansion(TestInfo info) throws IOException { Path temp = Files.createTempDirectory("fileExpansion"); File file1 = temp.resolve("beers1.csv").toFile(); - org.apache.commons.io.FileUtils - .copyInputStreamToFile(getClass().getClassLoader().getResourceAsStream("beers1.csv"), file1); + IOUtils.copy(getClass().getClassLoader().getResourceAsStream("beers1.csv"), new FileOutputStream(file1)); File file2 = temp.resolve("beers2.csv").toFile(); - org.apache.commons.io.FileUtils - .copyInputStreamToFile(getClass().getClassLoader().getResourceAsStream("beers2.csv"), file2); + IOUtils.copy(getClass().getClassLoader().getResourceAsStream("beers2.csv"), new FileOutputStream(file2)); FileImport executable = new FileImport(); - executable.setRedisOptions(redisClientOptions()); + executable.setRedisClientOptions(redisClientOptions()); executable.setFiles(temp.resolve("*.csv").toFile().getPath()); executable.setHeader(true); + executable.setName(name(info)); HsetBuilder hsetBuilder = new HsetBuilder(); - hsetBuilder.setKeyspace(keyspace); + hsetBuilder.setKeyspace(KEYSPACE); hsetBuilder.setKeyFields(ID); executable.setOperations(hsetBuilder.build()); executable.run(); @@ -102,7 +101,30 @@ void fileApiFileExpansion() throws IOException { for (String key : keys) { Map map = commands.hgetall(key); String id = map.get(ID); - assertEquals(key, keyspace + ":" + id); + assertEquals(key, KEYSPACE + ":" + id); + } + } + + @SuppressWarnings("unchecked") + @Test + void fileImportCSVMultiThreaded(TestInfo info) throws Exception { + FileImport executable = new FileImport(); + executable.setRedisClientOptions(redisClientOptions()); + executable.setFiles("https://storage.googleapis.com/jrx/beers.csv"); + executable.setHeader(true); + executable.setThreads(3); + executable.setName(name(info)); + HsetBuilder hset = new HsetBuilder(); + hset.setKeyspace(KEYSPACE); + hset.setKeyFields(ID); + executable.setOperations(hset.build()); + executable.run(); + List keys = commands.keys("*"); + assertEquals(2410, keys.size()); + for (String key : keys) { + Map map = commands.hgetall(key); + String id = map.get(ID); + assertEquals(key, KEYSPACE + ":" + id); } } diff --git a/connectors/riot-file/src/test/java/com/redis/riot/file/JsonSerdeTests.java b/connectors/riot-file/src/test/java/com/redis/riot/file/JsonSerdeTests.java new file mode 100644 index 000000000..cb37b668d --- /dev/null +++ b/connectors/riot-file/src/test/java/com/redis/riot/file/JsonSerdeTests.java @@ -0,0 +1,86 @@ +package com.redis.riot.file; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.util.unit.DataSize; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.redis.lettucemod.timeseries.Sample; +import com.redis.spring.batch.common.DataType; +import com.redis.spring.batch.common.KeyValue; +import com.redis.spring.batch.gen.GeneratorItemReader; +import com.redis.spring.batch.test.AbstractTestBase; + +@TestInstance(Lifecycle.PER_CLASS) +class JsonSerdeTests { + + private static final String timeseries = "{\"key\":\"gen:97\",\"type\":\"timeseries\",\"value\":[{\"timestamp\":1695939533285,\"value\":0.07027403662738285},{\"timestamp\":1695939533286,\"value\":0.7434808603018632},{\"timestamp\":1695939533287,\"value\":0.36481049906367213},{\"timestamp\":1695939533288,\"value\":0.08986928499552382},{\"timestamp\":1695939533289,\"value\":0.3901401870373925},{\"timestamp\":1695939533290,\"value\":0.1088584873055678},{\"timestamp\":1695939533291,\"value\":0.5649631025302376},{\"timestamp\":1695939533292,\"value\":0.9284983053028953},{\"timestamp\":1695939533293,\"value\":0.5009349293022067},{\"timestamp\":1695939533294,\"value\":0.7798297389022721}],\"ttl\":-1,\"memoryUsage\":0}"; + + private ObjectMapper mapper = new ObjectMapper(); + + @BeforeAll + void setup() { + mapper.configure(DeserializationFeature.USE_LONG_FOR_INTS, true); + SimpleModule module = new SimpleModule(); + module.addDeserializer(KeyValue.class, new KeyValueDeserializer()); + mapper.registerModule(module); + } + + @SuppressWarnings("unchecked") + @Test + void deserialize() throws JsonMappingException, JsonProcessingException { + KeyValue keyValue = mapper.readValue(timeseries, KeyValue.class); + Assertions.assertEquals("gen:97", keyValue.getKey()); + } + + @Test + void serialize() throws JsonProcessingException { + String key = "ts:1"; + long memoryUsage = DataSize.ofGigabytes(1).toBytes(); + long ttl = Instant.now().toEpochMilli(); + KeyValue ts = new KeyValue<>(); + ts.setKey(key); + ts.setMemoryUsage(memoryUsage); + ts.setTtl(ttl); + ts.setType(DataType.TIMESERIES); + Sample sample1 = Sample.of(Instant.now().toEpochMilli(), 123.456); + Sample sample2 = Sample.of(Instant.now().toEpochMilli() + 1000, 456.123); + ts.setValue(Arrays.asList(sample1, sample2)); + String json = mapper.writeValueAsString(ts); + JsonNode jsonNode = mapper.readTree(json); + Assertions.assertEquals(key, jsonNode.get("key").asText()); + ArrayNode valueNode = (ArrayNode) jsonNode.get("value"); + Assertions.assertEquals(2, valueNode.size()); + Assertions.assertEquals(sample2.getValue(), ((DoubleNode) valueNode.get(1).get("value")).asDouble()); + } + + @SuppressWarnings("unchecked") + @Test + void serde() throws Exception { + GeneratorItemReader reader = new GeneratorItemReader(); + reader.setMaxItemCount(17); + reader.open(new ExecutionContext()); + List> items = AbstractTestBase.readAll(reader); + for (KeyValue item : items) { + String json = mapper.writeValueAsString(item); + KeyValue result = mapper.readValue(json, KeyValue.class); + Assertions.assertEquals(item, result); + } + } + +} diff --git a/connectors/riot-file/src/test/java/com/redis/riot/file/StackFileTests.java b/connectors/riot-file/src/test/java/com/redis/riot/file/StackFileTests.java index e24924eb7..a0022d4dc 100644 --- a/connectors/riot-file/src/test/java/com/redis/riot/file/StackFileTests.java +++ b/connectors/riot-file/src/test/java/com/redis/riot/file/StackFileTests.java @@ -1,10 +1,8 @@ package com.redis.riot.file; -import com.redis.spring.batch.common.DataType; -import com.redis.spring.batch.test.AbstractTestBase; import com.redis.testcontainers.RedisStackContainer; -class StackFileTests extends FileTests { +class StackFileTests extends AbstractFileTests { private static final RedisStackContainer redis = new RedisStackContainer( RedisStackContainer.DEFAULT_IMAGE_NAME.withTag(RedisStackContainer.DEFAULT_TAG)); @@ -14,9 +12,4 @@ protected RedisStackContainer getRedisServer() { return redis; } - @Override - protected DataType[] generatorDataTypes() { - return AbstractTestBase.REDIS_MODULES_GENERATOR_TYPES; - } - } diff --git a/connectors/riot-redis/gradle.properties b/connectors/riot-redis/gradle.properties new file mode 100644 index 000000000..6fee5b1c1 --- /dev/null +++ b/connectors/riot-redis/gradle.properties @@ -0,0 +1,19 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2022-2023 The RIOT 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. +# +project_description = RIOT Redis +automatic.module.name = com.redis.riot.redis diff --git a/connectors/riot-redis/riot-redis.gradle b/connectors/riot-redis/riot-redis.gradle new file mode 100644 index 000000000..5fe83a2b8 --- /dev/null +++ b/connectors/riot-redis/riot-redis.gradle @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2020-2023 The RIOT 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. + */ +config { + licensing { + enabled = false + } +} + +dependencies { + implementation project(':riot-core') + implementation group: 'org.latencyutils', name: 'LatencyUtils', version: latencyutilsVersion + testImplementation group: 'com.redis', name: 'spring-batch-redis', version: springBatchRedisVersion, classifier: 'tests' +} + +compileJava { + options.compilerArgs += ["-AprojectPath=${project.group}/${project.name}"] +} + +if (!(project.findProperty('automatic.module.name.skip') ?: false).toBoolean()) { + jar { + manifest { + attributes('Automatic-Module-Name': project.findProperty('automatic.module.name')) + } + } +} + +bootJar { + enabled = false +} + +jar { + enabled = true + archiveClassifier = '' +} diff --git a/connectors/riot-redis/src/main/java/com/redis/riot/redis/CompareMode.java b/connectors/riot-redis/src/main/java/com/redis/riot/redis/CompareMode.java new file mode 100644 index 000000000..b273922b6 --- /dev/null +++ b/connectors/riot-redis/src/main/java/com/redis/riot/redis/CompareMode.java @@ -0,0 +1,5 @@ +package com.redis.riot.redis; + +public enum CompareMode { + FULL, QUICK, NONE +} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/GeneratorImport.java b/connectors/riot-redis/src/main/java/com/redis/riot/redis/GeneratorImport.java similarity index 97% rename from core/riot-core/src/main/java/com/redis/riot/core/GeneratorImport.java rename to connectors/riot-redis/src/main/java/com/redis/riot/redis/GeneratorImport.java index c53fd4106..d08acbfe8 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/GeneratorImport.java +++ b/connectors/riot-redis/src/main/java/com/redis/riot/redis/GeneratorImport.java @@ -1,9 +1,10 @@ -package com.redis.riot.core; +package com.redis.riot.redis; import java.util.List; import org.springframework.batch.core.Job; +import com.redis.riot.core.AbstractStructImport; import com.redis.spring.batch.RedisItemWriter; import com.redis.spring.batch.common.DataType; import com.redis.spring.batch.common.KeyValue; @@ -35,9 +36,9 @@ public class GeneratorImport extends AbstractStructImport { private ZsetOptions zsetOptions = new ZsetOptions(); @Override - protected Job job(RiotContext context) throws Exception { + protected Job job() { GeneratorItemReader reader = reader(); - RedisItemWriter> writer = writer(context); + RedisItemWriter> writer = writer(); return jobBuilder().start(step(getName(), reader, null, writer).build()).build(); } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/KeyComparisonDiffLogger.java b/connectors/riot-redis/src/main/java/com/redis/riot/redis/KeyComparisonDiffLogger.java similarity index 97% rename from core/riot-core/src/main/java/com/redis/riot/core/KeyComparisonDiffLogger.java rename to connectors/riot-redis/src/main/java/com/redis/riot/redis/KeyComparisonDiffLogger.java index 3ab547620..ad8310ccf 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/KeyComparisonDiffLogger.java +++ b/connectors/riot-redis/src/main/java/com/redis/riot/redis/KeyComparisonDiffLogger.java @@ -1,4 +1,4 @@ -package com.redis.riot.core; +package com.redis.riot.redis; import java.util.stream.StreamSupport; diff --git a/core/riot-core/src/main/java/com/redis/riot/core/KeyComparisonStatusCountItemWriter.java b/connectors/riot-redis/src/main/java/com/redis/riot/redis/KeyComparisonStatusCountItemWriter.java similarity index 98% rename from core/riot-core/src/main/java/com/redis/riot/core/KeyComparisonStatusCountItemWriter.java rename to connectors/riot-redis/src/main/java/com/redis/riot/redis/KeyComparisonStatusCountItemWriter.java index 3486719c5..19d65cf9c 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/KeyComparisonStatusCountItemWriter.java +++ b/connectors/riot-redis/src/main/java/com/redis/riot/redis/KeyComparisonStatusCountItemWriter.java @@ -1,4 +1,4 @@ -package com.redis.riot.core; +package com.redis.riot.redis; import java.util.List; import java.util.Map; diff --git a/core/riot-core/src/main/java/com/redis/riot/core/KeyComparisonSummaryLogger.java b/connectors/riot-redis/src/main/java/com/redis/riot/redis/KeyComparisonSummaryLogger.java similarity index 97% rename from core/riot-core/src/main/java/com/redis/riot/core/KeyComparisonSummaryLogger.java rename to connectors/riot-redis/src/main/java/com/redis/riot/redis/KeyComparisonSummaryLogger.java index c0a49e29e..0b1a9425c 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/KeyComparisonSummaryLogger.java +++ b/connectors/riot-redis/src/main/java/com/redis/riot/redis/KeyComparisonSummaryLogger.java @@ -1,4 +1,4 @@ -package com.redis.riot.core; +package com.redis.riot.redis; import java.util.List; diff --git a/core/riot-core/src/main/java/com/redis/riot/core/KeyValueWriteListener.java b/connectors/riot-redis/src/main/java/com/redis/riot/redis/KeyValueWriteListener.java similarity index 97% rename from core/riot-core/src/main/java/com/redis/riot/core/KeyValueWriteListener.java rename to connectors/riot-redis/src/main/java/com/redis/riot/redis/KeyValueWriteListener.java index 7112c5d35..17cc586f8 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/KeyValueWriteListener.java +++ b/connectors/riot-redis/src/main/java/com/redis/riot/redis/KeyValueWriteListener.java @@ -1,4 +1,4 @@ -package com.redis.riot.core; +package com.redis.riot.redis; import java.util.List; import java.util.function.Function; @@ -17,7 +17,6 @@ public class KeyValueWriteListener> implements ItemWriteListener { private final Logger log; - private final Function toStringKeyFunction; public KeyValueWriteListener(RedisCodec codec, Logger log) { diff --git a/connectors/riot-redis/src/main/java/com/redis/riot/redis/Ping.java b/connectors/riot-redis/src/main/java/com/redis/riot/redis/Ping.java new file mode 100644 index 000000000..d1681c475 --- /dev/null +++ b/connectors/riot-redis/src/main/java/com/redis/riot/redis/Ping.java @@ -0,0 +1,127 @@ +package com.redis.riot.redis; + +import java.io.PrintWriter; +import java.time.Duration; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; + +import org.HdrHistogram.Histogram; +import org.LatencyUtils.LatencyStats; +import org.springframework.util.Assert; + +import com.redis.riot.core.AbstractRunnable; + +import io.lettuce.core.metrics.CommandMetrics.CommandLatency; +import io.lettuce.core.metrics.DefaultCommandLatencyCollectorOptions; + +public class Ping extends AbstractRunnable { + + public static final int DEFAULT_ITERATIONS = 1; + public static final int DEFAULT_COUNT = 10; + public static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.MILLISECONDS; + private static final double[] DEFAULT_PERCENTILES = DefaultCommandLatencyCollectorOptions.DEFAULT_TARGET_PERCENTILES; + + private PrintWriter out; + private int iterations = DEFAULT_ITERATIONS; + private int count = DEFAULT_COUNT; + private TimeUnit timeUnit = DEFAULT_TIME_UNIT; + private boolean latencyDistribution; + private double[] percentiles = DEFAULT_PERCENTILES; + private Duration sleep; + + public void setOut(PrintWriter out) { + this.out = out; + } + + public void setSleep(Duration sleep) { + this.sleep = sleep; + } + + public Duration getSleep() { + return sleep; + } + + public int getIterations() { + return iterations; + } + + public void setIterations(int iterations) { + this.iterations = iterations; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public TimeUnit getTimeUnit() { + return timeUnit; + } + + public void setTimeUnit(TimeUnit unit) { + this.timeUnit = unit; + } + + public boolean isLatencyDistribution() { + return latencyDistribution; + } + + public void setLatencyDistribution(boolean latencyDistribution) { + this.latencyDistribution = latencyDistribution; + } + + public double[] getPercentiles() { + return percentiles; + } + + public void setPercentiles(double[] percentiles) { + this.percentiles = percentiles; + } + + @Override + protected void doRun() { + for (int iteration = 0; iteration < iterations; iteration++) { + LatencyStats stats = new LatencyStats(); + for (int index = 0; index < count; index++) { + long startTime = System.nanoTime(); + String reply = getRedisConnection().sync().ping(); + Assert.isTrue("pong".equalsIgnoreCase(reply), "Invalid PING reply received: " + reply); + stats.recordLatency(System.nanoTime() - startTime); + } + Histogram histogram = stats.getIntervalHistogram(); + if (latencyDistribution) { + histogram.outputPercentileDistribution(System.out, (double) timeUnit.toNanos(1)); + } + Map percentileMap = new TreeMap<>(); + for (double targetPercentile : percentiles) { + long percentile = toTimeUnit(histogram.getValueAtPercentile(targetPercentile)); + percentileMap.put(targetPercentile, percentile); + } + long min = toTimeUnit(histogram.getMinValue()); + long max = toTimeUnit(histogram.getMaxValue()); + CommandLatency latency = new CommandLatency(min, max, percentileMap); + out.println(latency.toString()); + if (sleep != null) { + try { + Thread.sleep(sleep.toMillis()); + } catch (InterruptedException e) { + // Restore interrupted state... + Thread.currentThread().interrupt(); + } + } + } + } + + private long toTimeUnit(long value) { + return timeUnit.convert(value, TimeUnit.NANOSECONDS); + } + + public static double[] defaultPercentiles() { + return DEFAULT_PERCENTILES; + } + +} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/Replication.java b/connectors/riot-redis/src/main/java/com/redis/riot/redis/Replication.java similarity index 61% rename from core/riot-core/src/main/java/com/redis/riot/core/Replication.java rename to connectors/riot-redis/src/main/java/com/redis/riot/redis/Replication.java index 86f0c3c4a..d76851265 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/Replication.java +++ b/connectors/riot-redis/src/main/java/com/redis/riot/redis/Replication.java @@ -1,4 +1,4 @@ -package com.redis.riot.core; +package com.redis.riot.redis; import java.time.Duration; @@ -10,14 +10,16 @@ import org.springframework.batch.core.job.builder.SimpleJobBuilder; import org.springframework.batch.core.job.flow.support.SimpleFlow; import org.springframework.batch.core.step.builder.FaultTolerantStepBuilder; -import org.springframework.batch.core.step.tasklet.TaskletStep; import org.springframework.batch.item.ItemProcessor; import org.springframework.batch.item.function.FunctionItemProcessor; import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.core.task.TaskExecutor; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.util.Assert; +import com.redis.lettucemod.api.StatefulRedisModulesConnection; +import com.redis.lettucemod.util.RedisModulesUtils; +import com.redis.riot.core.AbstractExport; +import com.redis.riot.core.RedisClientOptions; +import com.redis.riot.core.RedisWriterOptions; import com.redis.spring.batch.RedisItemReader; import com.redis.spring.batch.RedisItemWriter; import com.redis.spring.batch.common.KeyComparison; @@ -34,6 +36,7 @@ import io.lettuce.core.AbstractRedisClient; import io.lettuce.core.ReadFrom; import io.lettuce.core.RedisException; +import io.lettuce.core.RedisURI; import io.lettuce.core.codec.ByteArrayCodec; import io.lettuce.core.codec.StringCodec; @@ -47,8 +50,8 @@ public class Replication extends AbstractExport { public static final String STEP_SCAN = "scan"; public static final String STEP_COMPARE = "compare"; - private static final String VARIABLE_SOURCE = "source"; - private static final String VARIABLE_TARGET = "target"; + private static final String SOURCE_VAR = "source"; + private static final String TARGET_VAR = "target"; private final Logger log = LoggerFactory.getLogger(Replication.class); @@ -57,110 +60,80 @@ public class Replication extends AbstractExport { private boolean showDiffs; private CompareMode compareMode = DEFAULT_COMPARE_MODE; private Duration ttlTolerance = KeyComparisonItemReader.DEFAULT_TTL_TOLERANCE; - private RedisOptions targetRedisOptions = new RedisOptions(); + private RedisClientOptions targetRedisClientOptions = new RedisClientOptions(); private ReadFrom targetReadFrom; private RedisWriterOptions writerOptions = new RedisWriterOptions(); - public CompareMode getCompareMode() { - return compareMode; - } - - public void setCompareMode(CompareMode mode) { - this.compareMode = mode; - } - - public boolean isShowDiffs() { - return showDiffs; - } - - public void setShowDiffs(boolean showDiff) { - this.showDiffs = showDiff; - } - - public Duration getTtlTolerance() { - return ttlTolerance; - } - - public void setTtlTolerance(Duration ttlTolerance) { - this.ttlTolerance = ttlTolerance; - } - - public RedisOptions getTargetRedisOptions() { - return targetRedisOptions; - } - - public void setTargetRedisOptions(RedisOptions targetRedisOptions) { - this.targetRedisOptions = targetRedisOptions; - } - - public ReadFrom getTargetReadFrom() { - return targetReadFrom; - } - - public void setTargetReadFrom(ReadFrom targetReadFrom) { - this.targetReadFrom = targetReadFrom; - } + private RedisURI targetRedisURI; + private AbstractRedisClient targetRedisClient; + private StatefulRedisModulesConnection targetRedisConnection; - public RedisWriterOptions getWriterOptions() { - return writerOptions; - } - - public void setWriterOptions(RedisWriterOptions writerOptions) { - this.writerOptions = writerOptions; - } - - public ReplicationMode getMode() { - return mode; - } - - public ReplicationType getType() { - return type; - } - - public void setMode(ReplicationMode mode) { - this.mode = mode; + @Override + protected boolean isStruct() { + return type == ReplicationType.STRUCT; } - public void setType(ReplicationType type) { - this.type = type; + @Override + protected void open() { + super.open(); + targetRedisURI = targetRedisClientOptions.redisURI(); + targetRedisClient = targetRedisClientOptions.client(targetRedisURI); + targetRedisConnection = RedisModulesUtils.connection(targetRedisClient); } @Override - protected boolean isStruct() { - return type == ReplicationType.STRUCT; + protected StandardEvaluationContext evaluationContext() { + StandardEvaluationContext evaluationContext = super.evaluationContext(); + evaluationContext.setVariable(SOURCE_VAR, getRedisURI()); + evaluationContext.setVariable(TARGET_VAR, targetRedisURI); + return evaluationContext; } @Override - protected RiotContext createContext() { - ReplicationContext context = new ReplicationContext(super.createContext(), redisContext(targetRedisOptions)); - StandardEvaluationContext evaluationContext = context.getEvaluationContext(); - evaluationContext.setVariable(VARIABLE_SOURCE, context.getRedisContext().getUri()); - evaluationContext.setVariable(VARIABLE_TARGET, context.getTargetRedisContext().getUri()); - return context; + protected void close() { + try { + targetRedisConnection.close(); + } finally { + targetRedisClient.close(); + targetRedisClient.getResources().shutdown(); + } + super.close(); } @Override - protected Job job(RiotContext context) throws Exception { - Assert.isInstanceOf(ReplicationContext.class, context, "Execution context is not a replication context"); - ReplicationContext replicationContext = (ReplicationContext) context; + protected Job job() { + FaultTolerantStepBuilder, KeyValue> scanStep = step(STEP_SCAN, reader("scan-reader")); + RedisItemReader> reader = reader("live-reader"); + reader.setMode(RedisItemReader.Mode.LIVE); + FaultTolerantStepBuilder, KeyValue> liveStep = step(STEP_LIVE, reader); + KeyComparisonStatusCountItemWriter comparisonWriter = new KeyComparisonStatusCountItemWriter(); + FaultTolerantStepBuilder compareStep = step(STEP_COMPARE, comparisonReader(), + null, comparisonWriter); + if (showDiffs) { + compareStep.listener(new KeyComparisonDiffLogger()); + } + compareStep.listener(new KeyComparisonSummaryLogger(comparisonWriter)); switch (mode) { case COMPARE: - return jobBuilder().start(compareStep(replicationContext)).build(); + return jobBuilder().start(compareStep.build()).build(); case LIVE: - SimpleFlow scanFlow = flow("scan").start(scanStep(replicationContext).build()).build(); - SimpleFlow liveFlow = flow("live").start(liveStep(replicationContext).build()).build(); - SimpleFlow replicateFlow = flow("replicate").split(asyncTaskExecutor()).add(liveFlow, scanFlow).build(); + checkKeyspaceNotificationEnabled(); + SimpleFlow scanFlow = flow("scan").start(scanStep.build()).build(); + SimpleFlow liveFlow = flow("live").start(liveStep.build()).build(); + SimpleFlow replicateFlow = flow("replicate").split(new SimpleAsyncTaskExecutor()).add(liveFlow, scanFlow) + .build(); JobFlowBuilder live = jobBuilder().start(replicateFlow); if (shouldCompare()) { - live.next(compareStep(replicationContext)); + live.next(compareStep.build()); } return live.build().build(); case LIVEONLY: - return jobBuilder().start(liveStep(replicationContext).build()).build(); + checkKeyspaceNotificationEnabled(); + return jobBuilder().start(liveStep.build()).build(); case SNAPSHOT: - SimpleJobBuilder snapshot = jobBuilder().start(scanStep(replicationContext).build()); + SimpleJobBuilder snapshot = jobBuilder().start(scanStep.build()); if (shouldCompare()) { - snapshot.next(compareStep(replicationContext)); + snapshot.next(compareStep.build()); } return snapshot.build(); default: @@ -168,10 +141,6 @@ protected Job job(RiotContext context) throws Exception { } } - private TaskExecutor asyncTaskExecutor() { - return new SimpleAsyncTaskExecutor(); - } - private FlowBuilder flow(String name) { return new FlowBuilder<>(name(name)); } @@ -180,16 +149,11 @@ private boolean shouldCompare() { return compareMode != CompareMode.NONE && !isDryRun(); } - private FaultTolerantStepBuilder, KeyValue> scanStep(ReplicationContext context) - throws Exception { - return step(context, STEP_SCAN, reader("scan-reader", context.getRedisContext())); - } - - private FaultTolerantStepBuilder, KeyValue> step(ReplicationContext context, String name, - RedisItemReader> reader) throws Exception { - RedisItemWriter> writer = writer(context); + private FaultTolerantStepBuilder, KeyValue> step(String name, + RedisItemReader> reader) { + RedisItemWriter> writer = writer(); ItemProcessor, KeyValue> processor = new FunctionItemProcessor<>( - processor(ByteArrayCodec.INSTANCE, context)); + processor(ByteArrayCodec.INSTANCE)); FaultTolerantStepBuilder, KeyValue> step = step(name, reader, processor, writer); if (log.isDebugEnabled()) { step.listener(new KeyValueWriteListener<>(reader.getCodec(), log)); @@ -197,17 +161,9 @@ private FaultTolerantStepBuilder, KeyValue> step(Replic return step; } - private FaultTolerantStepBuilder, KeyValue> liveStep(ReplicationContext context) - throws Exception { - checkKeyspaceNotificationEnabled(context); - RedisItemReader> reader = reader("live-reader", context.getRedisContext()); - reader.setMode(RedisItemReader.Mode.LIVE); - return step(context, STEP_LIVE, reader); - } - - private void checkKeyspaceNotificationEnabled(ReplicationContext context) { + private void checkKeyspaceNotificationEnabled() { try { - String config = context.getRedisContext().getConnection().sync().configGet(CONFIG_NOTIFY_KEYSPACE_EVENTS) + String config = getRedisConnection().sync().configGet(CONFIG_NOTIFY_KEYSPACE_EVENTS) .getOrDefault(CONFIG_NOTIFY_KEYSPACE_EVENTS, ""); if (!config.contains("K")) { log.error( @@ -219,10 +175,9 @@ private void checkKeyspaceNotificationEnabled(ReplicationContext context) { } } - private RedisItemReader> reader(String name, RedisContext context) - throws Exception { - AbstractKeyValueItemReader reader = reader(context.getClient()); - configureReader(name(name), reader, context); + private RedisItemReader> reader(String name) { + AbstractKeyValueItemReader reader = reader(getRedisClient()); + configureReader(name(name), reader); return reader; } @@ -233,30 +188,17 @@ private AbstractKeyValueItemReader reader(AbstractRedisClient cl return new DumpItemReader(client); } - private TaskletStep compareStep(ReplicationContext context) throws Exception { - KeyComparisonItemReader reader = comparisonReader(context); - KeyComparisonStatusCountItemWriter writer = new KeyComparisonStatusCountItemWriter(); - FaultTolerantStepBuilder step = step(STEP_COMPARE, reader, null, writer); - if (showDiffs) { - step.listener(new KeyComparisonDiffLogger()); - } - step.listener(new KeyComparisonSummaryLogger(writer)); - return step.build(); - } - - private KeyComparisonItemReader comparisonReader(ReplicationContext context) throws Exception { - AbstractKeyValueItemReader sourceReader = comparisonKeyValueReader( - context.getRedisContext().getClient()); - configureReader("source-comparison-reader", sourceReader, context.getRedisContext()); - AbstractKeyValueItemReader targetReader = comparisonKeyValueReader( - context.getTargetRedisContext().getClient()); + private KeyComparisonItemReader comparisonReader() { + AbstractKeyValueItemReader sourceReader = comparisonKeyValueReader(getRedisClient()); + configureReader("source-comparison-reader", sourceReader); + AbstractKeyValueItemReader targetReader = comparisonKeyValueReader(targetRedisClient); targetReader.setReadFrom(targetReadFrom); targetReader.setPoolSize(writerOptions.getPoolSize()); KeyComparisonItemReader comparisonReader = new KeyComparisonItemReader(sourceReader, targetReader); - configureReader("comparison-reader", comparisonReader, context.getRedisContext()); - comparisonReader.setProcessor(processor(StringCodec.UTF8, context)); + configureReader("comparison-reader", comparisonReader); + comparisonReader.setProcessor(processor(StringCodec.UTF8)); comparisonReader.setTtlTolerance(ttlTolerance); - comparisonReader.setCompareStreamMessageIds(!processorOptions.isDropStreamMessageId()); + comparisonReader.setCompareStreamMessageIds(!getProcessorOptions().isDropStreamMessageId()); return comparisonReader; } @@ -267,8 +209,7 @@ private AbstractKeyValueItemReader comparisonKeyValueReader(Abst return new KeyTypeItemReader<>(client, StringCodec.UTF8); } - private RedisItemWriter> writer(ReplicationContext context) { - AbstractRedisClient targetRedisClient = context.getTargetRedisContext().getClient(); + private RedisItemWriter> writer() { KeyValueItemWriter writer = writer(targetRedisClient); return writer(writer, writerOptions); } @@ -280,4 +221,68 @@ private KeyValueItemWriter writer(AbstractRedisClient client) { return new DumpItemWriter(client); } + public CompareMode getCompareMode() { + return compareMode; + } + + public void setCompareMode(CompareMode mode) { + this.compareMode = mode; + } + + public boolean isShowDiffs() { + return showDiffs; + } + + public void setShowDiffs(boolean showDiff) { + this.showDiffs = showDiff; + } + + public Duration getTtlTolerance() { + return ttlTolerance; + } + + public void setTtlTolerance(Duration ttlTolerance) { + this.ttlTolerance = ttlTolerance; + } + + public RedisClientOptions getTargetRedisClientOptions() { + return targetRedisClientOptions; + } + + public void setTargetRedisClientOptions(RedisClientOptions targetRedisOptions) { + this.targetRedisClientOptions = targetRedisOptions; + } + + public ReadFrom getTargetReadFrom() { + return targetReadFrom; + } + + public void setTargetReadFrom(ReadFrom targetReadFrom) { + this.targetReadFrom = targetReadFrom; + } + + public RedisWriterOptions getWriterOptions() { + return writerOptions; + } + + public void setWriterOptions(RedisWriterOptions writerOptions) { + this.writerOptions = writerOptions; + } + + public ReplicationMode getMode() { + return mode; + } + + public ReplicationType getType() { + return type; + } + + public void setMode(ReplicationMode mode) { + this.mode = mode; + } + + public void setType(ReplicationType type) { + this.type = type; + } + } diff --git a/connectors/riot-redis/src/main/java/com/redis/riot/redis/ReplicationMode.java b/connectors/riot-redis/src/main/java/com/redis/riot/redis/ReplicationMode.java new file mode 100644 index 000000000..ddb756e23 --- /dev/null +++ b/connectors/riot-redis/src/main/java/com/redis/riot/redis/ReplicationMode.java @@ -0,0 +1,5 @@ +package com.redis.riot.redis; + +public enum ReplicationMode { + SNAPSHOT, LIVE, LIVEONLY, COMPARE +} diff --git a/connectors/riot-redis/src/main/java/com/redis/riot/redis/ReplicationType.java b/connectors/riot-redis/src/main/java/com/redis/riot/redis/ReplicationType.java new file mode 100644 index 000000000..024e5d0d4 --- /dev/null +++ b/connectors/riot-redis/src/main/java/com/redis/riot/redis/ReplicationType.java @@ -0,0 +1,5 @@ +package com.redis.riot.redis; + +public enum ReplicationType { + DUMP, STRUCT +} diff --git a/core/riot-core/src/test/java/com/redis/riot/core/test/AbstractReplicationTests.java b/connectors/riot-redis/src/test/java/com/redis/riot/redis/AbstractReplicationTests.java similarity index 87% rename from core/riot-core/src/test/java/com/redis/riot/core/test/AbstractReplicationTests.java rename to connectors/riot-redis/src/test/java/com/redis/riot/redis/AbstractReplicationTests.java index afb4266a9..7767ad516 100644 --- a/core/riot-core/src/test/java/com/redis/riot/core/test/AbstractReplicationTests.java +++ b/connectors/riot-redis/src/test/java/com/redis/riot/redis/AbstractReplicationTests.java @@ -1,4 +1,4 @@ -package com.redis.riot.core.test; +package com.redis.riot.redis; import java.time.Duration; import java.util.Map; @@ -9,11 +9,9 @@ import org.junit.jupiter.api.TestInfo; import org.springframework.batch.item.support.ListItemWriter; -import com.redis.riot.core.KeyValueProcessorOptions; +import com.redis.riot.core.ExportProcessorOptions; import com.redis.riot.core.PredicateItemProcessor; -import com.redis.riot.core.RedisOptions; -import com.redis.riot.core.Replication; -import com.redis.riot.core.ReplicationType; +import com.redis.riot.core.RedisClientOptions; import com.redis.riot.core.RiotUtils; import com.redis.spring.batch.RedisItemReader; import com.redis.spring.batch.common.KeyComparisonItemReader; @@ -48,13 +46,13 @@ protected void execute(Replication replication, TestInfo info) { replication.setName(name(info)); replication.setJobRepository(jobRepository); replication.setTransactionManager(transactionManager); - replication.setRedisOptions(redisOptions(getRedisServer())); - replication.setTargetRedisOptions(redisOptions(getTargetRedisServer())); + replication.setRedisClientOptions(redisOptions(getRedisServer())); + replication.setTargetRedisClientOptions(redisOptions(getTargetRedisServer())); replication.run(); } - private RedisOptions redisOptions(RedisServer redis) { - RedisOptions options = new RedisOptions(); + private RedisClientOptions redisOptions(RedisServer redis) { + RedisClientOptions options = new RedisClientOptions(); options.setUri(redis.getRedisURI()); options.setCluster(redis.isRedisCluster()); return options; @@ -62,7 +60,7 @@ private RedisOptions redisOptions(RedisServer redis) { @Test void replication(TestInfo info) throws Throwable { - generate(info); + generate(info, generator(73)); Assertions.assertTrue(commands.dbsize() > 0); Replication replication = new Replication(); execute(replication, info); @@ -81,8 +79,8 @@ void keyProcessor(TestInfo info) throws Throwable { Assertions.assertEquals(value1, targetCommands.get("string:" + key1)); } - private KeyValueProcessorOptions processorOptions(String keyExpression) { - KeyValueProcessorOptions options = new KeyValueProcessorOptions(); + private ExportProcessorOptions processorOptions(String keyExpression) { + ExportProcessorOptions options = new ExportProcessorOptions(); options.setKeyExpression(RiotUtils.parseTemplate(keyExpression)); return options; } diff --git a/core/riot-core/src/test/java/com/redis/riot/core/test/RedisContainerFactory.java b/connectors/riot-redis/src/test/java/com/redis/riot/redis/RedisContainerFactory.java similarity index 95% rename from core/riot-core/src/test/java/com/redis/riot/core/test/RedisContainerFactory.java rename to connectors/riot-redis/src/test/java/com/redis/riot/redis/RedisContainerFactory.java index a657cf08f..3308ce7b7 100644 --- a/core/riot-core/src/test/java/com/redis/riot/core/test/RedisContainerFactory.java +++ b/connectors/riot-redis/src/test/java/com/redis/riot/redis/RedisContainerFactory.java @@ -1,4 +1,4 @@ -package com.redis.riot.core.test; +package com.redis.riot.redis; import com.redis.enterprise.Database; import com.redis.enterprise.RedisModule; diff --git a/connectors/riot-redis/src/test/java/com/redis/riot/redis/StackTests.java b/connectors/riot-redis/src/test/java/com/redis/riot/redis/StackTests.java new file mode 100644 index 000000000..ae0d7c21f --- /dev/null +++ b/connectors/riot-redis/src/test/java/com/redis/riot/redis/StackTests.java @@ -0,0 +1,20 @@ +package com.redis.riot.redis; + +import com.redis.testcontainers.RedisStackContainer; + +class StackTests extends AbstractReplicationTests { + + private static final RedisStackContainer source = RedisContainerFactory.stack(); + private static final RedisStackContainer target = RedisContainerFactory.stack(); + + @Override + protected RedisStackContainer getRedisServer() { + return source; + } + + @Override + protected RedisStackContainer getTargetRedisServer() { + return target; + } + +} diff --git a/core/riot-core/riot-core.gradle b/core/riot-core/riot-core.gradle index c301f1239..be8f0b7fd 100644 --- a/core/riot-core/riot-core.gradle +++ b/core/riot-core/riot-core.gradle @@ -29,10 +29,7 @@ dependencies { api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' implementation 'org.springframework.boot:spring-boot-autoconfigure' implementation 'org.hsqldb:hsqldb' - implementation group: 'org.latencyutils', name: 'LatencyUtils', version: latencyutilsVersion implementation 'org.apache.commons:commons-pool2' - testImplementation 'org.slf4j:slf4j-simple' - testImplementation group: 'com.redis', name: 'spring-batch-redis', version: springBatchRedisVersion, classifier: 'tests' testImplementation 'org.awaitility:awaitility' } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/AbstractExport.java b/core/riot-core/src/main/java/com/redis/riot/core/AbstractExport.java index 4d4d7f3a3..5d0cb698d 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/AbstractExport.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/AbstractExport.java @@ -6,7 +6,7 @@ import org.springframework.batch.item.ItemProcessor; import org.springframework.batch.item.support.PassThroughItemProcessor; -import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; import com.redis.riot.core.function.DropStreamMessageIdFunction; import com.redis.riot.core.function.ExpressionFunction; @@ -23,43 +23,44 @@ public abstract class AbstractExport extends AbstractJobRunnable { - private RedisReaderOptions readerOptions = new RedisReaderOptions(); - - protected KeyValueProcessorOptions processorOptions = new KeyValueProcessorOptions(); - - public void setReaderOptions(RedisReaderOptions readerOptions) { - this.readerOptions = readerOptions; - } + private static final String REDIS_VAR = "redis"; - public void setProcessorOptions(KeyValueProcessorOptions options) { - this.processorOptions = options; - } + private EvaluationContextOptions evaluationContextOptions = new EvaluationContextOptions(); + private RedisReaderOptions readerOptions = new RedisReaderOptions(); + private ExportProcessorOptions processorOptions = new ExportProcessorOptions(); - protected Function, KeyValue> processor(RedisCodec codec, RiotContext context) { + protected Function, KeyValue> processor(RedisCodec codec) { ToStringKeyValueFunction code = new ToStringKeyValueFunction<>(codec); StringKeyValueFunction decode = new StringKeyValueFunction<>(codec); - UnaryOperator> function = keyValueOperator(context.getEvaluationContext()); + UnaryOperator> function = keyValueOperator(); return code.andThen(function).andThen(decode); + } + protected StandardEvaluationContext evaluationContext() { + StandardEvaluationContext evaluationContext = evaluationContextOptions.evaluationContext(); + evaluationContext.setVariable(REDIS_VAR, getRedisConnection().sync()); + return evaluationContext; } - private UnaryOperator> keyValueOperator(EvaluationContext context) { + private UnaryOperator> keyValueOperator() { KeyValueOperator operator = new KeyValueOperator(); + StandardEvaluationContext evaluationContext = evaluationContext(); if (processorOptions.getKeyExpression() != null) { - operator.setKeyFunction(ExpressionFunction.of(context, processorOptions.getKeyExpression())); + operator.setKeyFunction(ExpressionFunction.of(evaluationContext, processorOptions.getKeyExpression())); } if (processorOptions.isDropTtl()) { operator.setTtlFunction(t -> 0); } else { if (processorOptions.getTtlExpression() != null) { - operator.setTtlFunction(new LongExpressionFunction<>(context, processorOptions.getTtlExpression())); + operator.setTtlFunction( + new LongExpressionFunction<>(evaluationContext, processorOptions.getTtlExpression())); } } if (processorOptions.isDropStreamMessageId() && isStruct()) { operator.setValueFunction(new DropStreamMessageIdFunction()); } if (processorOptions.getTypeExpression() != null) { - Function, String> function = ExpressionFunction.of(context, + Function, String> function = ExpressionFunction.of(evaluationContext, processorOptions.getTypeExpression()); operator.setTypeFunction(function.andThen(DataType::of)); } @@ -68,13 +69,12 @@ private UnaryOperator> keyValueOperator(EvaluationContext conte protected abstract boolean isStruct(); - protected > R configureReader(String name, R reader, RedisContext context) - throws Exception { + protected > R configureReader(String name, R reader) { reader.setName(name); - reader.setJobRepository(jobRepository()); - reader.setTransactionManager(transactionManager()); + reader.setJobRepository(getJobRepository()); + reader.setTransactionManager(getTransactionManager()); reader.setChunkSize(readerOptions.getChunkSize()); - reader.setDatabase(context.getUri().getDatabase()); + reader.setDatabase(getRedisURI().getDatabase()); reader.setKeyProcessor(keyFilteringProcessor(reader.getCodec())); reader.setKeyPattern(readerOptions.getKeyPattern()); reader.setKeyType(readerOptions.getKeyType()); @@ -103,4 +103,24 @@ public ItemProcessor keyFilteringProcessor(RedisCodec codec) { return new PredicateItemProcessor<>(predicate); } + public RedisReaderOptions getReaderOptions() { + return readerOptions; + } + + public void setReaderOptions(RedisReaderOptions readerOptions) { + this.readerOptions = readerOptions; + } + + public ExportProcessorOptions getProcessorOptions() { + return processorOptions; + } + + public void setProcessorOptions(ExportProcessorOptions options) { + this.processorOptions = options; + } + + public void setEvaluationContextOptions(EvaluationContextOptions spelProcessorOptions) { + this.evaluationContextOptions = spelProcessorOptions; + } + } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/AbstractImport.java b/core/riot-core/src/main/java/com/redis/riot/core/AbstractImport.java index e4a4c6319..318bbb051 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/AbstractImport.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/AbstractImport.java @@ -1,31 +1,15 @@ package com.redis.riot.core; -import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.function.Function; -import java.util.function.Predicate; import java.util.stream.Collectors; import org.springframework.batch.item.ItemProcessor; import org.springframework.batch.item.ItemWriter; -import org.springframework.batch.item.function.FunctionItemProcessor; -import org.springframework.context.expression.MapAccessor; -import org.springframework.expression.AccessException; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.expression.TypedValue; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import com.redis.lettucemod.util.GeoLocation; -import com.redis.riot.core.function.ExpressionFunction; -import com.redis.riot.core.function.MapFunction; import com.redis.spring.batch.RedisItemWriter; import com.redis.spring.batch.common.Operation; import com.redis.spring.batch.writer.OperationItemWriter; @@ -34,97 +18,51 @@ public abstract class AbstractImport extends AbstractJobRunnable { - private Map processorExpressions; - - private Expression filterExpression; - - public Map getProcessorExpressions() { - return processorExpressions; - } + private RedisWriterOptions writerOptions = new RedisWriterOptions(); + private EvaluationContextOptions evaluationContextOptions = new EvaluationContextOptions(); + private ImportProcessorOptions processorOptions = new ImportProcessorOptions(); + private List, Object>> operations; - public void setProcessorExpressions(Map expressions) { - this.processorExpressions = expressions; + @SuppressWarnings("unchecked") + public void setOperations(Operation, Object>... operations) { + setOperations(Arrays.asList(operations)); } - public Expression getFilterExpression() { - return filterExpression; + public List, Object>> getOperations() { + return operations; } - public void setFilterExpression(Expression filter) { - this.filterExpression = filter; + public void setOperations(List, Object>> operations) { + this.operations = operations; } - public ItemProcessor, Map> processor(StandardEvaluationContext context) { - context.addPropertyAccessor(new QuietMapAccessor()); - try { - context.registerFunction("geo", - GeoLocation.class.getDeclaredMethod("toString", String.class, String.class)); - } catch (NoSuchMethodException e) { - // ignore - } - List, Map>> processors = new ArrayList<>(); - if (!CollectionUtils.isEmpty(processorExpressions)) { - Map, Object>> functions = new LinkedHashMap<>(); - for (Entry field : processorExpressions.entrySet()) { - functions.put(field.getKey(), new ExpressionFunction<>(context, field.getValue(), Object.class)); - } - processors.add(new FunctionItemProcessor<>(new MapFunction(functions))); - } - if (filterExpression != null) { - Predicate> predicate = RiotUtils.predicate(context, filterExpression); - processors.add(new PredicateItemProcessor<>(predicate)); - } - return RiotUtils.processor(processors); + public EvaluationContextOptions getEvaluationContextOptions() { + return evaluationContextOptions; } - /** - * {@link org.springframework.context.expression.MapAccessor} that always - * returns true for canRead and does not throw AccessExceptions - * - */ - public static class QuietMapAccessor extends MapAccessor { - - @Override - public boolean canRead(EvaluationContext context, @Nullable Object target, String name) { - return true; - } - - @Override - public TypedValue read(EvaluationContext context, @Nullable Object target, String name) { - try { - return super.read(context, target, name); - } catch (AccessException e) { - return new TypedValue(null); - } - } - + public void setEvaluationContextOptions(EvaluationContextOptions evaluationContextOptions) { + this.evaluationContextOptions = evaluationContextOptions; } - private RedisWriterOptions writerOptions = new RedisWriterOptions(); - - private List, Object>> operations; - - protected ItemProcessor, Map> processor(RiotContext context) { - return processor(context.getEvaluationContext()); + public RedisWriterOptions getWriterOptions() { + return writerOptions; } - @SuppressWarnings("unchecked") - public void setOperations(Operation, Object>... operations) { - setOperations(Arrays.asList(operations)); + public void setWriterOptions(RedisWriterOptions options) { + this.writerOptions = options; } - public void setOperations(List, Object>> operations) { - this.operations = operations; + public ImportProcessorOptions getProcessorOptions() { + return processorOptions; } - public void setWriterOptions(RedisWriterOptions operationOptions) { - this.writerOptions = operationOptions; + public void setProcessorOptions(ImportProcessorOptions options) { + this.processorOptions = options; } - protected ItemWriter> writer(RiotContext context) { + protected ItemWriter> writer() { Assert.notEmpty(operations, "No operation specified"); - AbstractRedisClient client = context.getRedisContext().getClient(); - return RiotUtils.writer(operations.stream().map(o -> writer(client, o)).collect(Collectors.toList())); + return RiotUtils.writer(operations.stream().map(o -> writer(getRedisClient(), o)).collect(Collectors.toList())); } private ItemWriter writer(AbstractRedisClient client, Operation operation) { @@ -132,4 +70,12 @@ private ItemWriter writer(AbstractRedisClient client, Operation, Map> processor() { + return processorOptions.processor(evaluationContext()); + } + + protected StandardEvaluationContext evaluationContext() { + return evaluationContextOptions.evaluationContext(); + } + } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/AbstractJobRunnable.java b/core/riot-core/src/main/java/com/redis/riot/core/AbstractJobRunnable.java index 9ee044bb7..a66ea4c08 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/AbstractJobRunnable.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/AbstractJobRunnable.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.UUID; import java.util.function.Consumer; import javax.sql.DataSource; @@ -13,6 +12,7 @@ import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionException; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.job.builder.JobBuilder; @@ -28,11 +28,12 @@ import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemStreamReader; import org.springframework.batch.item.ItemWriter; +import org.springframework.batch.item.support.SynchronizedItemReader; import org.springframework.batch.item.support.SynchronizedItemStreamReader; import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.autoconfigure.batch.BatchDataSourceScriptDatabaseInitializer; -import org.springframework.boot.autoconfigure.batch.BatchProperties; +import org.springframework.boot.autoconfigure.batch.BatchProperties.Jdbc; +import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer; import org.springframework.boot.sql.init.DatabaseInitializationMode; import org.springframework.core.task.SyncTaskExecutor; import org.springframework.core.task.TaskExecutor; @@ -49,7 +50,7 @@ import io.lettuce.core.RedisCommandExecutionException; import io.lettuce.core.RedisCommandTimeoutException; -public abstract class AbstractJobRunnable extends AbstractRiotRunnable { +public abstract class AbstractJobRunnable extends AbstractRunnable { public static final SkipPolicy DEFAULT_SKIP_POLICY = new NeverSkipItemSkipPolicy(); public static final int DEFAULT_SKIP_LIMIT = 0; @@ -60,7 +61,7 @@ public abstract class AbstractJobRunnable extends AbstractRiotRunnable { private static final String FAILED_JOB_MESSAGE = "Error executing job %s"; - private String name = String.format("%s-%s", ClassUtils.getShortName(getClass()), UUID.randomUUID().toString()); + private String name; private Consumer> stepConfigurer; private int threads = DEFAULT_THREADS; private int chunkSize = DEFAULT_CHUNK_SIZE; @@ -68,10 +69,12 @@ public abstract class AbstractJobRunnable extends AbstractRiotRunnable { private boolean dryRun; private int skipLimit = DEFAULT_SKIP_LIMIT; private int retryLimit = DEFAULT_RETRY_LIMIT; - private JobRepository jobRepository; private PlatformTransactionManager transactionManager; - private TaskExecutorJobLauncher jobLauncher; + + protected AbstractJobRunnable() { + setName(ClassUtils.getShortName(getClass())); + } protected String name(String... suffixes) { List elements = new ArrayList<>(); @@ -80,31 +83,59 @@ protected String name(String... suffixes) { return String.join("-", elements); } + public void setStepConfigurer(Consumer> stepConfigurer) { + this.stepConfigurer = stepConfigurer; + } + public void setJobRepository(JobRepository jobRepository) { this.jobRepository = jobRepository; } - public void setTransactionManager(PlatformTransactionManager transactionManager) { - this.transactionManager = transactionManager; + public void setTransactionManager(PlatformTransactionManager platformTransactionManager) { + this.transactionManager = platformTransactionManager; } - public void setStepConfigurer(Consumer> stepConfigurer) { - this.stepConfigurer = stepConfigurer; + @Override + protected void open() { + super.open(); + if (transactionManager == null) { + transactionManager = new ResourcelessTransactionManager(); + } + if (jobRepository == null) { + JobRepositoryFactoryBean bean = new JobRepositoryFactoryBean(); + bean.setDataSource(dataSource()); + bean.setDatabaseType("HSQL"); + bean.setTransactionManager(transactionManager); + try { + bean.afterPropertiesSet(); + jobRepository = bean.getObject(); + } catch (Exception e) { + throw new ExecutionException("Could not initialize job repository", e); + } + } + } + + private DataSource dataSource() { + JDBCDataSource source = new JDBCDataSource(); + source.setURL("jdbc:hsqldb:mem:" + name); + Jdbc jdbc = new Jdbc(); + jdbc.setInitializeSchema(DatabaseInitializationMode.ALWAYS); + AbstractScriptDatabaseInitializer initializer = new BatchDataSourceScriptDatabaseInitializer(source, jdbc); + initializer.initializeDatabase(); + return source; } @Override - protected void execute(RiotContext executionContext) { - Job job; - try { - job = job(executionContext); - } catch (Exception e) { - throw new RiotException("Could not create job", e); - } + protected void doRun() { + Job job = job(); + TaskExecutorJobLauncher jobLauncher = new TaskExecutorJobLauncher(); + jobLauncher.setJobRepository(jobRepository); + jobLauncher.setTaskExecutor(new SyncTaskExecutor()); JobExecution jobExecution; try { - jobExecution = jobLauncher().run(job, new JobParameters()); - } catch (Exception e) { - throw new RiotException(String.format(FAILED_JOB_MESSAGE, job.getName()), e); + jobExecution = jobLauncher.run(job, new JobParameters()); + } catch (JobExecutionException e) { + throw new ExecutionException(String.format(FAILED_JOB_MESSAGE, job.getName()), e); } if (jobExecution.getExitStatus().getExitCode().equals(ExitStatus.FAILED.getExitCode())) { for (StepExecution stepExecution : jobExecution.getStepExecutions()) { @@ -113,63 +144,23 @@ protected void execute(RiotContext executionContext) { String message = String.format("Error executing step %s in job %s: %s", stepExecution.getStepName(), job.getName(), exitStatus.getExitDescription()); if (stepExecution.getFailureExceptions().isEmpty()) { - throw new RiotException(message); + throw new ExecutionException(message); } - throw new RiotException(message, stepExecution.getFailureExceptions().get(0)); + throw new ExecutionException(message, stepExecution.getFailureExceptions().get(0)); } } if (jobExecution.getAllFailureExceptions().isEmpty()) { - throw new RiotException(String.format("Error executing job %s: %s", job.getName(), + throw new ExecutionException(String.format("Error executing job %s: %s", job.getName(), jobExecution.getExitStatus().getExitDescription())); } } } - protected JobBuilder jobBuilder() throws Exception { - return new JobBuilder(name, jobRepository()); + protected JobBuilder jobBuilder() { + return new JobBuilder(getName(), jobRepository); } - private TaskExecutorJobLauncher jobLauncher() throws Exception { - if (jobLauncher == null) { - jobLauncher = new TaskExecutorJobLauncher(); - jobLauncher.setJobRepository(jobRepository()); - jobLauncher.setTaskExecutor(new SyncTaskExecutor()); - } - return jobLauncher; - } - - protected PlatformTransactionManager transactionManager() { - if (transactionManager == null) { - transactionManager = new ResourcelessTransactionManager(); - } - return transactionManager; - } - - protected JobRepository jobRepository() throws Exception { - if (jobRepository == null) { - JobRepositoryFactoryBean bean = new JobRepositoryFactoryBean(); - bean.setDataSource(dataSource()); - bean.setDatabaseType("HSQL"); - bean.setTransactionManager(transactionManager()); - bean.afterPropertiesSet(); - jobRepository = bean.getObject(); - } - return jobRepository; - } - - private DataSource dataSource() throws Exception { - JDBCDataSource dataSource = new JDBCDataSource(); - dataSource.setURL("jdbc:hsqldb:mem:" + name); - BatchProperties.Jdbc jdbc = new BatchProperties.Jdbc(); - jdbc.setInitializeSchema(DatabaseInitializationMode.ALWAYS); - BatchDataSourceScriptDatabaseInitializer initializer = new BatchDataSourceScriptDatabaseInitializer(dataSource, - jdbc); - initializer.afterPropertiesSet(); - initializer.initializeDatabase(); - return dataSource; - } - - protected abstract Job job(RiotContext executionContext) throws Exception; + protected abstract Job job(); protected > W writer(W writer, RedisWriterOptions options) { writer.setMultiExec(options.isMultiExec()); @@ -183,7 +174,7 @@ private DataSource dataSource() throws Exception { } protected FaultTolerantStepBuilder step(String name, ItemReader reader, - ItemProcessor processor, ItemWriter writer) throws Exception { + ItemProcessor processor, ItemWriter writer) { RiotStep riotStep = new RiotStep<>(); riotStep.setName(name); riotStep.setReader(reader); @@ -192,10 +183,10 @@ protected FaultTolerantStepBuilder step(String name, ItemReader if (stepConfigurer != null) { stepConfigurer.accept(riotStep); } - SimpleStepBuilder step = new StepBuilder(riotStep.getName(), jobRepository()).chunk(chunkSize, + SimpleStepBuilder step = new StepBuilder(riotStep.getName(), jobRepository).chunk(chunkSize, transactionManager); step.reader(reader(riotStep.getReader())); - step.processor(processor(riotStep.getProcessor())); + step.processor(riotStep.getProcessor()); step.writer(writer(riotStep.getWriter())); step.taskExecutor(taskExecutor()); riotStep.getConfigurer().accept(step); @@ -219,50 +210,40 @@ private FaultTolerantStepBuilder faultTolerant(SimpleStepBuilder 1) { + if (isMultiThreaded()) { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setMaxPoolSize(threads); taskExecutor.setCorePoolSize(threads); taskExecutor.setQueueCapacity(threads); - taskExecutor.afterPropertiesSet(); + taskExecutor.initialize(); return taskExecutor; } return new SyncTaskExecutor(); } - private ItemProcessor processor(ItemProcessor processor) { - initializeBean(processor); - return processor; - } - - private void initializeBean(Object object) { - if (object instanceof InitializingBean) { - try { - ((InitializingBean) object).afterPropertiesSet(); - } catch (Exception e) { - throw new RiotException("Could not initialize " + object, e); - } - } - } - private ItemReader reader(ItemReader reader) { - initializeBean(reader); if (reader instanceof RedisItemReader) { return reader; } - if (threads > 1 && reader instanceof ItemStreamReader) { - SynchronizedItemStreamReader synchronizedReader = new SynchronizedItemStreamReader<>(); - synchronizedReader.setDelegate((ItemStreamReader) reader); - return synchronizedReader; + if (isMultiThreaded()) { + if (reader instanceof ItemStreamReader) { + SynchronizedItemStreamReader synchronizedReader = new SynchronizedItemStreamReader<>(); + synchronizedReader.setDelegate((ItemStreamReader) reader); + return synchronizedReader; + } + return new SynchronizedItemReader<>(reader); } return reader; } + private boolean isMultiThreaded() { + return threads > 1; + } + private ItemWriter writer(ItemWriter writer) { if (dryRun) { return new NoopItemWriter<>(); } - initializeBean(writer); if (RiotUtils.isPositive(sleep)) { return new ThrottledItemWriter<>(writer, sleep); } @@ -325,4 +306,12 @@ public void setName(String name) { this.name = name; } + protected JobRepository getJobRepository() { + return jobRepository; + } + + protected PlatformTransactionManager getTransactionManager() { + return transactionManager; + } + } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/AbstractMapExport.java b/core/riot-core/src/main/java/com/redis/riot/core/AbstractMapExport.java index 6b5d21b62..4f03dc543 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/AbstractMapExport.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/AbstractMapExport.java @@ -26,22 +26,22 @@ public void setKeyRegex(Pattern pattern) { } @Override - protected Job job(RiotContext context) throws Exception { - StructItemReader reader = reader(context.getRedisContext()); - ItemProcessor, Map> processor = processor(context); + protected Job job() { + StructItemReader reader = reader(); + ItemProcessor, Map> processor = processor(); ItemWriter> writer = writer(); return jobBuilder().start(step(getName(), reader, processor, writer).build()).build(); } - protected StructItemReader reader(RedisContext context) throws Exception { - StructItemReader reader = new StructItemReader<>(context.getClient(), StringCodec.UTF8); - configureReader("export-reader", reader, context); + protected StructItemReader reader() { + StructItemReader reader = new StructItemReader<>(getRedisClient(), StringCodec.UTF8); + configureReader("export-reader", reader); return reader; } - protected ItemProcessor, Map> processor(RiotContext context) { + protected ItemProcessor, Map> processor() { ItemProcessor, KeyValue> processor = new FunctionItemProcessor<>( - processor(StringCodec.UTF8, context)); + processor(StringCodec.UTF8)); StructToMapFunction toMapFunction = new StructToMapFunction(); if (keyRegex != null) { toMapFunction.setKey(new RegexNamedGroupFunction(keyRegex)); diff --git a/core/riot-core/src/main/java/com/redis/riot/core/AbstractRiotRunnable.java b/core/riot-core/src/main/java/com/redis/riot/core/AbstractRiotRunnable.java deleted file mode 100644 index 1468ab2f7..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/AbstractRiotRunnable.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.redis.riot.core; - -import java.text.SimpleDateFormat; -import java.time.Duration; -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.expression.Expression; -import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -import com.redis.lettucemod.RedisModulesClient; -import com.redis.lettucemod.cluster.RedisModulesClusterClient; - -import io.lettuce.core.AbstractRedisClient; -import io.lettuce.core.ClientOptions; -import io.lettuce.core.RedisURI; -import io.lettuce.core.SslOptions; -import io.lettuce.core.SslOptions.Builder; -import io.lettuce.core.SslOptions.Resource; -import io.lettuce.core.cluster.ClusterClientOptions; -import io.lettuce.core.event.DefaultEventPublisherOptions; -import io.lettuce.core.event.EventPublisherOptions; -import io.lettuce.core.metrics.CommandLatencyCollector; -import io.lettuce.core.metrics.CommandLatencyRecorder; -import io.lettuce.core.metrics.DefaultCommandLatencyCollectorOptions; -import io.lettuce.core.resource.ClientResources; -import io.lettuce.core.resource.DefaultClientResources; - -public abstract class AbstractRiotRunnable implements Runnable { - - public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; - private static final String DATE_VAR = "date"; - private static final String REDIS_VAR = "redis"; - - private RedisOptions redisOptions = new RedisOptions(); - private String dateFormat = DEFAULT_DATE_FORMAT; - private Map varExpressions = new LinkedHashMap<>(); - private Map vars = new LinkedHashMap<>(); - - public String getDateFormat() { - return dateFormat; - } - - public void setDateFormat(String format) { - this.dateFormat = format; - } - - public Map getVarExpressions() { - return varExpressions; - } - - public void setVarExpressions(Map expressions) { - this.varExpressions = expressions; - } - - public Map getVars() { - return vars; - } - - public void setVars(Map variables) { - this.vars = variables; - } - - public RedisOptions getRedisOptions() { - return redisOptions; - } - - public void setRedisOptions(RedisOptions options) { - this.redisOptions = options; - } - - @Override - public void run() { - try (RiotContext context = createContext()) { - execute(context); - } - } - - protected RiotContext createContext() { - RedisContext redisContext = redisContext(redisOptions); - StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); - evaluationContext.setVariable(DATE_VAR, new SimpleDateFormat(dateFormat)); - evaluationContext.setVariable(REDIS_VAR, redisContext.getConnection().sync()); - if (!CollectionUtils.isEmpty(vars)) { - vars.forEach(evaluationContext::setVariable); - } - if (!CollectionUtils.isEmpty(varExpressions)) { - varExpressions.forEach((k, v) -> evaluationContext.setVariable(k, v.getValue(evaluationContext))); - } - return new RiotContext(redisContext, evaluationContext); - } - - protected RedisContext redisContext(RedisOptions options) { - RedisURI redisURI = redisURI(options); - AbstractRedisClient client = client(redisURI, options); - return new RedisContext(redisURI, client); - } - - protected abstract void execute(RiotContext executionContext); - - public RedisURI redisURI(RedisOptions options) { - RedisURI.Builder builder = redisURIBuilder(options); - if (options.getDatabase() > 0) { - builder.withDatabase(options.getDatabase()); - } - if (StringUtils.hasLength(options.getClientName())) { - builder.withClientName(options.getClientName()); - } - if (!ObjectUtils.isEmpty(options.getPassword())) { - if (StringUtils.hasLength(options.getUsername())) { - builder.withAuthentication(options.getUsername(), options.getPassword()); - } else { - builder.withPassword(options.getPassword()); - } - } - if (options.isTls()) { - builder.withSsl(options.isTls()); - builder.withVerifyPeer(options.getVerifyPeer()); - } - if (options.getTimeout() != null) { - builder.withTimeout(options.getTimeout()); - } - return builder.build(); - } - - private RedisURI.Builder redisURIBuilder(RedisOptions options) { - if (StringUtils.hasLength(options.getUri())) { - return RedisURI.builder(RedisURI.create(options.getUri())); - } - if (StringUtils.hasLength(options.getSocket())) { - return RedisURI.Builder.socket(options.getSocket()); - } - return RedisURI.Builder.redis(options.getHost(), options.getPort()); - } - - private AbstractRedisClient client(RedisURI redisURI, RedisOptions options) { - ClientResources resources = clientResources(options); - if (options.isCluster()) { - RedisModulesClusterClient client = RedisModulesClusterClient.create(resources, redisURI); - client.setOptions(clientOptions(ClusterClientOptions.builder(), options).build()); - return client; - } - RedisModulesClient client = RedisModulesClient.create(resources, redisURI); - client.setOptions(clientOptions(ClientOptions.builder(), options).build()); - return client; - } - - private B clientOptions(B builder, RedisOptions options) { - builder.autoReconnect(options.isAutoReconnect()); - builder.sslOptions(sslOptions(options)); - builder.protocolVersion(options.getProtocolVersion()); - return builder; - } - - public SslOptions sslOptions(RedisOptions options) { - Builder ssl = SslOptions.builder(); - if (options.getKey() != null) { - ssl.keyManager(options.getKeyCert(), options.getKey(), options.getKeyPassword()); - } - if (options.getKeystore() != null) { - ssl.keystore(options.getKeystore(), options.getKeystorePassword()); - } - if (options.getTruststore() != null) { - ssl.truststore(Resource.from(options.getTruststore()), options.getTruststorePassword()); - } - if (options.getTrustedCerts() != null) { - ssl.trustManager(options.getTrustedCerts()); - } - return ssl.build(); - } - - private ClientResources clientResources(RedisOptions options) { - DefaultClientResources.Builder builder = DefaultClientResources.builder(); - if (options.getMetricsStep() != null) { - builder.commandLatencyRecorder(commandLatencyRecorder()); - builder.commandLatencyPublisherOptions(commandLatencyPublisherOptions(options.getMetricsStep())); - } - return builder.build(); - } - - private EventPublisherOptions commandLatencyPublisherOptions(Duration step) { - return DefaultEventPublisherOptions.builder().eventEmitInterval(step).build(); - } - - private CommandLatencyRecorder commandLatencyRecorder() { - return CommandLatencyCollector.create(DefaultCommandLatencyCollectorOptions.builder().enable().build()); - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/AbstractRunnable.java b/core/riot-core/src/main/java/com/redis/riot/core/AbstractRunnable.java new file mode 100644 index 000000000..0dbf62a9e --- /dev/null +++ b/core/riot-core/src/main/java/com/redis/riot/core/AbstractRunnable.java @@ -0,0 +1,65 @@ +package com.redis.riot.core; + +import com.redis.lettucemod.api.StatefulRedisModulesConnection; +import com.redis.lettucemod.util.RedisModulesUtils; + +import io.lettuce.core.AbstractRedisClient; +import io.lettuce.core.RedisURI; + +public abstract class AbstractRunnable implements Runnable { + + private RedisClientOptions redisClientOptions = new RedisClientOptions(); + + private RedisURI redisURI; + private AbstractRedisClient redisClient; + private StatefulRedisModulesConnection redisConnection; + + @Override + public void run() { + try { + open(); + } catch (Exception e) { + throw new ExecutionException("Could not initialize RIOT", e); + } + doRun(); + close(); + } + + protected abstract void doRun(); + + protected void open() { + redisURI = redisClientOptions.redisURI(); + redisClient = redisClientOptions.client(redisURI); + redisConnection = RedisModulesUtils.connection(redisClient); + } + + protected void close() { + try { + redisConnection.close(); + } finally { + redisClient.close(); + redisClient.getResources().shutdown(); + } + } + + public RedisClientOptions getRedisClientOptions() { + return redisClientOptions; + } + + public void setRedisClientOptions(RedisClientOptions options) { + this.redisClientOptions = options; + } + + protected RedisURI getRedisURI() { + return redisURI; + } + + protected AbstractRedisClient getRedisClient() { + return redisClient; + } + + protected StatefulRedisModulesConnection getRedisConnection() { + return redisConnection; + } + +} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/AbstractStructImport.java b/core/riot-core/src/main/java/com/redis/riot/core/AbstractStructImport.java index 8fda9c8e3..e44a59fa6 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/AbstractStructImport.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/AbstractStructImport.java @@ -4,21 +4,19 @@ import com.redis.spring.batch.common.KeyValue; import com.redis.spring.batch.writer.StructItemWriter; -import io.lettuce.core.AbstractRedisClient; import io.lettuce.core.codec.StringCodec; public abstract class AbstractStructImport extends AbstractJobRunnable { - private RedisWriterOptions writerOptions = new RedisWriterOptions(); + private RedisWriterOptions writerOptions = new RedisWriterOptions(); - public void setWriterOptions(RedisWriterOptions options) { - this.writerOptions = options; - } + public void setWriterOptions(RedisWriterOptions options) { + this.writerOptions = options; + } - protected RedisItemWriter> writer(RiotContext context) { - AbstractRedisClient client = context.getRedisContext().getClient(); - StructItemWriter writer = new StructItemWriter<>(client, StringCodec.UTF8); - return writer(writer, writerOptions); - } + protected RedisItemWriter> writer() { + StructItemWriter writer = new StructItemWriter<>(getRedisClient(), StringCodec.UTF8); + return writer(writer, writerOptions); + } } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/BlockedKeyItemWriter.java b/core/riot-core/src/main/java/com/redis/riot/core/BlockedKeyItemWriter.java deleted file mode 100644 index dccfb2bf2..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/BlockedKeyItemWriter.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.redis.riot.core; - -import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.StreamSupport; - -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ItemWriter; -import org.springframework.util.unit.DataSize; - -import com.redis.spring.batch.common.KeyValue; -import com.redis.spring.batch.util.CodecUtils; - -import io.lettuce.core.codec.RedisCodec; - -public class BlockedKeyItemWriter> implements ItemWriter { - - private final Set blockedKeys; - - private final Function toStringKeyFunction; - - private final Predicate predicate; - - public BlockedKeyItemWriter(RedisCodec codec, DataSize memoryUsageLimit, Set blockedKeys) { - this.toStringKeyFunction = CodecUtils.toStringKeyFunction(codec); - this.predicate = new BlockedKeyPredicate<>(memoryUsageLimit); - this.blockedKeys = blockedKeys; - } - - @Override - public void write(Chunk items) throws Exception { - StreamSupport.stream(items.spliterator(), false).filter(predicate).map(KeyValue::getKey) - .map(toStringKeyFunction).forEach(blockedKeys::add); - } - - private static class BlockedKeyPredicate> implements Predicate { - - private final long memLimit; - - public BlockedKeyPredicate(DataSize memoryUsageLimit) { - this.memLimit = memoryUsageLimit.toBytes(); - } - - @Override - public boolean test(T t) { - if (t == null) { - return false; - } - return t.getMemoryUsage() > memLimit; - } - - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/CompareMode.java b/core/riot-core/src/main/java/com/redis/riot/core/CompareMode.java deleted file mode 100644 index 645efaacf..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/CompareMode.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.redis.riot.core; - -public enum CompareMode { - - FULL, QUICK, NONE - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/EvaluationContextOptions.java b/core/riot-core/src/main/java/com/redis/riot/core/EvaluationContextOptions.java new file mode 100644 index 000000000..35c86a2fb --- /dev/null +++ b/core/riot-core/src/main/java/com/redis/riot/core/EvaluationContextOptions.java @@ -0,0 +1,66 @@ +package com.redis.riot.core; + +import java.text.SimpleDateFormat; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.CollectionUtils; + +public class EvaluationContextOptions { + + public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + + private static final String DEFAULT_DATE_VAR = "date"; + + private String dateVar = DEFAULT_DATE_VAR; + private String dateFormat = DEFAULT_DATE_FORMAT; + private Map varExpressions = new LinkedHashMap<>(); + private Map vars = new LinkedHashMap<>(); + + public String getDateVar() { + return dateVar; + } + + public void setDateVar(String dateVar) { + this.dateVar = dateVar; + } + + public String getDateFormat() { + return dateFormat; + } + + public void setDateFormat(String format) { + this.dateFormat = format; + } + + public Map getVarExpressions() { + return varExpressions; + } + + public void setVarExpressions(Map expressions) { + this.varExpressions = expressions; + } + + public Map getVars() { + return vars; + } + + public void setVars(Map variables) { + this.vars = variables; + } + + public StandardEvaluationContext evaluationContext() { + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + evaluationContext.setVariable(dateVar, new SimpleDateFormat(dateFormat)); + if (!CollectionUtils.isEmpty(vars)) { + vars.forEach(evaluationContext::setVariable); + } + if (!CollectionUtils.isEmpty(varExpressions)) { + varExpressions.forEach((k, v) -> evaluationContext.setVariable(k, v.getValue(evaluationContext))); + } + return evaluationContext; + } + +} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/ExecutionException.java b/core/riot-core/src/main/java/com/redis/riot/core/ExecutionException.java new file mode 100644 index 000000000..8d48bbaa1 --- /dev/null +++ b/core/riot-core/src/main/java/com/redis/riot/core/ExecutionException.java @@ -0,0 +1,19 @@ +package com.redis.riot.core; + +public class ExecutionException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ExecutionException(String message) { + super(message); + } + + public ExecutionException(String message, Throwable cause) { + super(message, cause); + } + + public ExecutionException(Throwable cause) { + super(cause); + } + +} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/ExportProcessorOptions.java b/core/riot-core/src/main/java/com/redis/riot/core/ExportProcessorOptions.java new file mode 100644 index 000000000..b23b0fe1a --- /dev/null +++ b/core/riot-core/src/main/java/com/redis/riot/core/ExportProcessorOptions.java @@ -0,0 +1,58 @@ +package com.redis.riot.core; + +import org.springframework.expression.Expression; + +public class ExportProcessorOptions { + + private TemplateExpression keyExpression; + private Expression ttlExpression; + private boolean dropTtl; + private Expression typeExpression; + private boolean dropStreamMessageId; + + public boolean isDropStreamMessageId() { + return dropStreamMessageId; + } + + public void setDropStreamMessageId(boolean dropStreamMessageId) { + this.dropStreamMessageId = dropStreamMessageId; + } + + public Expression getTypeExpression() { + return typeExpression; + } + + public void setTypeExpression(Expression expression) { + this.typeExpression = expression; + } + + public boolean isDropTtl() { + return dropTtl; + } + + public void setDropTtl(boolean dropTtl) { + this.dropTtl = dropTtl; + } + + public TemplateExpression getKeyExpression() { + return keyExpression; + } + + public void setKeyExpression(TemplateExpression expression) { + this.keyExpression = expression; + } + + public Expression getTtlExpression() { + return ttlExpression; + } + + public void setTtlExpression(Expression expression) { + this.ttlExpression = expression; + } + + public boolean isEmpty() { + return keyExpression == null && ttlExpression == null && !dropTtl && typeExpression == null + && !dropStreamMessageId; + } + +} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/ImportProcessorOptions.java b/core/riot-core/src/main/java/com/redis/riot/core/ImportProcessorOptions.java new file mode 100644 index 000000000..967653fd9 --- /dev/null +++ b/core/riot-core/src/main/java/com/redis/riot/core/ImportProcessorOptions.java @@ -0,0 +1,95 @@ +package com.redis.riot.core; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.function.FunctionItemProcessor; +import org.springframework.context.expression.MapAccessor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +import com.redis.lettucemod.util.GeoLocation; +import com.redis.riot.core.function.ExpressionFunction; +import com.redis.riot.core.function.MapFunction; + +public class ImportProcessorOptions { + + private Map processorExpressions; + private Expression filterExpression; + + public Map getProcessorExpressions() { + return processorExpressions; + } + + public void setProcessorExpressions(Map expressions) { + this.processorExpressions = expressions; + } + + public Expression getFilterExpression() { + return filterExpression; + } + + public void setFilterExpression(Expression filter) { + this.filterExpression = filter; + } + + public ItemProcessor, Map> processor( + StandardEvaluationContext evaluationContext) { + evaluationContext.addPropertyAccessor(new QuietMapAccessor()); + try { + evaluationContext.registerFunction("geo", + GeoLocation.class.getDeclaredMethod("toString", String.class, String.class)); + } catch (NoSuchMethodException e) { + // ignore + } + List, Map>> processors = new ArrayList<>(); + if (!CollectionUtils.isEmpty(processorExpressions)) { + Map, Object>> functions = new LinkedHashMap<>(); + for (Entry field : processorExpressions.entrySet()) { + functions.put(field.getKey(), + new ExpressionFunction<>(evaluationContext, field.getValue(), Object.class)); + } + processors.add(new FunctionItemProcessor<>(new MapFunction(functions))); + } + if (filterExpression != null) { + Predicate> predicate = RiotUtils.predicate(evaluationContext, filterExpression); + processors.add(new PredicateItemProcessor<>(predicate)); + } + return RiotUtils.processor(processors); + } + + /** + * {@link org.springframework.context.expression.MapAccessor} that always + * returns true for canRead and does not throw AccessExceptions + * + */ + private static class QuietMapAccessor extends MapAccessor { + + @Override + public boolean canRead(EvaluationContext context, @Nullable Object target, String name) { + return true; + } + + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) { + try { + return super.read(context, target, name); + } catch (AccessException e) { + return new TypedValue(null); + } + } + + } + +} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/KeyFilterOptions.java b/core/riot-core/src/main/java/com/redis/riot/core/KeyFilterOptions.java index 75e65543d..9d1b61d8d 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/KeyFilterOptions.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/KeyFilterOptions.java @@ -6,34 +6,32 @@ public class KeyFilterOptions { - private List includes; + private List includes; + private List excludes; + private List slots; - private List excludes; + public List getIncludes() { + return includes; + } - private List slots; + public void setIncludes(List patterns) { + this.includes = patterns; + } - public List getIncludes() { - return includes; - } + public List getExcludes() { + return excludes; + } - public void setIncludes(List patterns) { - this.includes = patterns; - } + public void setExcludes(List patterns) { + this.excludes = patterns; + } - public List getExcludes() { - return excludes; - } + public List getSlots() { + return slots; + } - public void setExcludes(List patterns) { - this.excludes = patterns; - } - - public List getSlots() { - return slots; - } - - public void setSlots(List ranges) { - this.slots = ranges; - } + public void setSlots(List ranges) { + this.slots = ranges; + } } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/KeyValueDeserializer.java b/core/riot-core/src/main/java/com/redis/riot/core/KeyValueDeserializer.java deleted file mode 100644 index e0836b1a3..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/KeyValueDeserializer.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.redis.riot.core; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; -import java.util.Set; - -import org.springframework.util.StringUtils; - -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.DoubleNode; -import com.fasterxml.jackson.databind.node.LongNode; -import com.redis.lettucemod.timeseries.Sample; -import com.redis.spring.batch.common.DataType; -import com.redis.spring.batch.common.KeyValue; - -import io.lettuce.core.ScoredValue; -import io.lettuce.core.StreamMessage; - -public class KeyValueDeserializer extends StdDeserializer> { - - private static final long serialVersionUID = 1L; - - public static final String KEY = "key"; - - public static final String TYPE = "type"; - - public static final String VALUE = "value"; - - public static final String SCORE = "score"; - - public static final String TTL = "ttl"; - - public static final String MEMORY_USAGE = "memoryUsage"; - - public static final String STREAM = "stream"; - - public static final String ID = "id"; - - public static final String BODY = "body"; - - public static final String TIMESTAMP = "timestamp"; - - public KeyValueDeserializer() { - this(null); - } - - public KeyValueDeserializer(Class> t) { - super(t); - } - - @Override - public KeyValue deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { - JsonNode node = p.getCodec().readTree(p); - KeyValue keyValue = new KeyValue<>(); - JsonNode keyNode = node.get(KEY); - if (keyNode != null) { - keyValue.setKey(node.get(KEY).asText()); - } - JsonNode typeNode = node.get(TYPE); - if (typeNode != null) { - String typeString = typeNode.asText(); - if (StringUtils.hasLength(typeString)) { - keyValue.setType(DataType.valueOf(typeString.toUpperCase())); - } - } - LongNode ttlNode = (LongNode) node.get(TTL); - if (ttlNode != null) { - keyValue.setTtl(ttlNode.asLong()); - } - LongNode memUsageNode = (LongNode) node.get(MEMORY_USAGE); - if (memUsageNode != null) { - keyValue.setMemoryUsage(memUsageNode.asLong()); - } - keyValue.setValue(value(keyValue.getType(), node.get(VALUE), ctxt)); - return keyValue; - } - - private Object value(DataType type, JsonNode node, DeserializationContext ctxt) throws IOException { - switch (type) { - case STREAM: - return streamMessages((ArrayNode) node, ctxt); - case ZSET: - return scoredValues((ArrayNode) node); - case TIMESERIES: - return samples((ArrayNode) node); - case HASH: - return ctxt.readTreeAsValue(node, Map.class); - case STRING: - case JSON: - return node.asText(); - case LIST: - return ctxt.readTreeAsValue(node, Collection.class); - case SET: - return ctxt.readTreeAsValue(node, Set.class); - default: - return null; - } - } - - private Collection samples(ArrayNode node) { - Collection samples = new ArrayList<>(node.size()); - for (int index = 0; index < node.size(); index++) { - JsonNode sampleNode = node.get(index); - if (sampleNode != null) { - samples.add(sample(sampleNode)); - } - } - return samples; - } - - private Sample sample(JsonNode node) { - LongNode timestampNode = (LongNode) node.get(TIMESTAMP); - long timestamp = timestampNode == null || timestampNode.isNull() ? 0 : timestampNode.asLong(); - DoubleNode valueNode = (DoubleNode) node.get(VALUE); - double value = valueNode == null || valueNode.isNull() ? 0 : valueNode.asDouble(); - return Sample.of(timestamp, value); - } - - private Collection> scoredValues(ArrayNode node) { - Collection> scoredValues = new ArrayList<>(node.size()); - for (int index = 0; index < node.size(); index++) { - JsonNode scoredValueNode = node.get(index); - if (scoredValueNode != null) { - scoredValues.add(scoredValue(scoredValueNode)); - } - } - return scoredValues; - } - - private ScoredValue scoredValue(JsonNode scoredValueNode) { - JsonNode valueNode = scoredValueNode.get(VALUE); - String value = valueNode == null || valueNode.isNull() ? null : valueNode.asText(); - DoubleNode scoreNode = (DoubleNode) scoredValueNode.get(SCORE); - double score = scoreNode == null || scoreNode.isNull() ? 0 : scoreNode.asDouble(); - return ScoredValue.just(score, value); - } - - private Collection> streamMessages(ArrayNode node, DeserializationContext ctxt) - throws IOException { - Collection> messages = new ArrayList<>(node.size()); - for (int index = 0; index < node.size(); index++) { - JsonNode messageNode = node.get(index); - if (messageNode != null) { - messages.add(streamMessage(messageNode, ctxt)); - } - } - return messages; - } - - @SuppressWarnings("unchecked") - private StreamMessage streamMessage(JsonNode messageNode, DeserializationContext ctxt) throws IOException { - JsonNode streamNode = messageNode.get(STREAM); - String stream = streamNode == null || streamNode.isNull() ? null : streamNode.asText(); - JsonNode bodyNode = messageNode.get(BODY); - Map body = ctxt.readTreeAsValue(bodyNode, Map.class); - JsonNode idNode = messageNode.get(ID); - String id = idNode == null || idNode.isNull() ? null : idNode.asText(); - return new StreamMessage<>(stream, id, body); - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/KeyValueProcessorOptions.java b/core/riot-core/src/main/java/com/redis/riot/core/KeyValueProcessorOptions.java deleted file mode 100644 index e3232993c..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/KeyValueProcessorOptions.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.redis.riot.core; - -import org.springframework.expression.Expression; - -public class KeyValueProcessorOptions { - - private TemplateExpression keyExpression; - - private Expression ttlExpression; - - private boolean dropTtl; - - private Expression typeExpression; - - private boolean dropStreamMessageId; - - public boolean isDropStreamMessageId() { - return dropStreamMessageId; - } - - public void setDropStreamMessageId(boolean dropStreamMessageId) { - this.dropStreamMessageId = dropStreamMessageId; - } - - public Expression getTypeExpression() { - return typeExpression; - } - - public void setTypeExpression(Expression expression) { - this.typeExpression = expression; - } - - public boolean isDropTtl() { - return dropTtl; - } - - public void setDropTtl(boolean dropTtl) { - this.dropTtl = dropTtl; - } - - public TemplateExpression getKeyExpression() { - return keyExpression; - } - - public void setKeyExpression(TemplateExpression expression) { - this.keyExpression = expression; - } - - public Expression getTtlExpression() { - return ttlExpression; - } - - public void setTtlExpression(Expression expression) { - this.ttlExpression = expression; - } - - public boolean isEmpty() { - return keyExpression == null && ttlExpression == null && !dropTtl && typeExpression == null && !dropStreamMessageId; - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/Ping.java b/core/riot-core/src/main/java/com/redis/riot/core/Ping.java deleted file mode 100644 index daa0afdba..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/Ping.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.redis.riot.core; - -import java.io.PrintWriter; -import java.time.Duration; -import java.util.Map; -import java.util.TreeMap; -import java.util.concurrent.TimeUnit; - -import org.HdrHistogram.Histogram; -import org.LatencyUtils.LatencyStats; -import org.springframework.util.Assert; - -import io.lettuce.core.metrics.CommandMetrics.CommandLatency; -import io.lettuce.core.metrics.DefaultCommandLatencyCollectorOptions; - -public class Ping extends AbstractRiotRunnable { - - public static final int DEFAULT_ITERATIONS = 1; - - public static final int DEFAULT_COUNT = 10; - - public static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.MILLISECONDS; - - private static final double[] DEFAULT_PERCENTILES = DefaultCommandLatencyCollectorOptions.DEFAULT_TARGET_PERCENTILES; - - private PrintWriter out; - - private int iterations = DEFAULT_ITERATIONS; - - private int count = DEFAULT_COUNT; - - private TimeUnit timeUnit = DEFAULT_TIME_UNIT; - - private boolean latencyDistribution; - - private double[] percentiles = DEFAULT_PERCENTILES; - - private Duration sleep; - - public void setOut(PrintWriter out) { - this.out = out; - } - - public void setSleep(Duration sleep) { - this.sleep = sleep; - } - - public Duration getSleep() { - return sleep; - } - - public int getIterations() { - return iterations; - } - - public void setIterations(int iterations) { - this.iterations = iterations; - } - - public int getCount() { - return count; - } - - public void setCount(int count) { - this.count = count; - } - - public TimeUnit getTimeUnit() { - return timeUnit; - } - - public void setTimeUnit(TimeUnit unit) { - this.timeUnit = unit; - } - - public boolean isLatencyDistribution() { - return latencyDistribution; - } - - public void setLatencyDistribution(boolean latencyDistribution) { - this.latencyDistribution = latencyDistribution; - } - - public double[] getPercentiles() { - return percentiles; - } - - public void setPercentiles(double[] percentiles) { - this.percentiles = percentiles; - } - - @Override - protected void execute(RiotContext executionContext) { - for (int iteration = 0; iteration < iterations; iteration++) { - LatencyStats stats = new LatencyStats(); - for (int index = 0; index < count; index++) { - long startTime = System.nanoTime(); - String reply = executionContext.getRedisContext().getConnection().sync().ping(); - Assert.isTrue("pong".equalsIgnoreCase(reply), "Invalid PING reply received: " + reply); - stats.recordLatency(System.nanoTime() - startTime); - } - Histogram histogram = stats.getIntervalHistogram(); - if (latencyDistribution) { - histogram.outputPercentileDistribution(System.out, (double) timeUnit.toNanos(1)); - } - Map percentileMap = new TreeMap<>(); - for (double targetPercentile : percentiles) { - long percentile = toTimeUnit(histogram.getValueAtPercentile(targetPercentile)); - percentileMap.put(targetPercentile, percentile); - } - long min = toTimeUnit(histogram.getMinValue()); - long max = toTimeUnit(histogram.getMaxValue()); - CommandLatency latency = new CommandLatency(min, max, percentileMap); - out.println(latency.toString()); - if (sleep != null) { - try { - Thread.sleep(sleep.toMillis()); - } catch (InterruptedException e) { - // Restore interrupted state... - Thread.currentThread().interrupt(); - } - } - } - } - - private long toTimeUnit(long value) { - return timeUnit.convert(value, TimeUnit.NANOSECONDS); - } - - public static double[] defaultPercentiles() { - return DEFAULT_PERCENTILES; - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/RedisOptions.java b/core/riot-core/src/main/java/com/redis/riot/core/RedisClientOptions.java similarity index 53% rename from core/riot-core/src/main/java/com/redis/riot/core/RedisOptions.java rename to core/riot-core/src/main/java/com/redis/riot/core/RedisClientOptions.java index 3322af829..951e7708f 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/RedisOptions.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/RedisClientOptions.java @@ -3,16 +3,35 @@ import java.io.File; import java.time.Duration; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import com.redis.lettucemod.RedisModulesClient; +import com.redis.lettucemod.cluster.RedisModulesClusterClient; + +import io.lettuce.core.AbstractRedisClient; import io.lettuce.core.ClientOptions; import io.lettuce.core.RedisURI; +import io.lettuce.core.SslOptions; +import io.lettuce.core.SslOptions.Builder; +import io.lettuce.core.SslOptions.Resource; import io.lettuce.core.SslVerifyMode; +import io.lettuce.core.cluster.ClusterClientOptions; +import io.lettuce.core.event.DefaultEventPublisherOptions; +import io.lettuce.core.event.EventPublisherOptions; +import io.lettuce.core.metrics.CommandLatencyCollector; +import io.lettuce.core.metrics.CommandLatencyRecorder; +import io.lettuce.core.metrics.DefaultCommandLatencyCollectorOptions; import io.lettuce.core.protocol.ProtocolVersion; +import io.lettuce.core.resource.ClientResources; +import io.lettuce.core.resource.DefaultClientResources; -public class RedisOptions { +public class RedisClientOptions { public static final String DEFAULT_HOST = "127.0.0.1"; public static final int DEFAULT_PORT = 6379; public static final SslVerifyMode DEFAULT_VERIFY_PEER = SslVerifyMode.FULL; + public static final Duration DEFAULT_TIMEOUT = RedisURI.DEFAULT_TIMEOUT_DURATION; private String uri; private String host = DEFAULT_HOST; @@ -20,11 +39,11 @@ public class RedisOptions { private String socket; private String username; private char[] password; - private Duration timeout = RedisURI.DEFAULT_TIMEOUT_DURATION; + private Duration timeout = DEFAULT_TIMEOUT; private int database; private boolean tls; - private SslVerifyMode verifyPeer = DEFAULT_VERIFY_PEER; private String clientName; + private SslVerifyMode verifyPeer = DEFAULT_VERIFY_PEER; private boolean cluster; private Duration metricsStep; private boolean autoReconnect = ClientOptions.DEFAULT_AUTO_RECONNECT; @@ -38,6 +57,94 @@ public class RedisOptions { private char[] keyPassword; private File trustedCerts; + public AbstractRedisClient client(RedisURI redisURI) { + ClientResources resources = clientResources(); + if (cluster) { + RedisModulesClusterClient client = RedisModulesClusterClient.create(resources, redisURI); + client.setOptions(clientOptions(ClusterClientOptions.builder()).build()); + return client; + } + RedisModulesClient client = RedisModulesClient.create(resources, redisURI); + client.setOptions(clientOptions(ClientOptions.builder()).build()); + return client; + } + + private B clientOptions(B builder) { + builder.autoReconnect(autoReconnect); + builder.sslOptions(sslOptions()); + builder.protocolVersion(protocolVersion); + return builder; + } + + public SslOptions sslOptions() { + Builder ssl = SslOptions.builder(); + if (key != null) { + ssl.keyManager(keyCert, key, keyPassword); + } + if (keystore != null) { + ssl.keystore(keystore, keystorePassword); + } + if (truststore != null) { + ssl.truststore(Resource.from(truststore), truststorePassword); + } + if (trustedCerts != null) { + ssl.trustManager(trustedCerts); + } + return ssl.build(); + } + + private ClientResources clientResources() { + DefaultClientResources.Builder builder = DefaultClientResources.builder(); + if (metricsStep != null) { + builder.commandLatencyRecorder(commandLatencyRecorder()); + builder.commandLatencyPublisherOptions(commandLatencyPublisherOptions(metricsStep)); + } + return builder.build(); + } + + private EventPublisherOptions commandLatencyPublisherOptions(Duration step) { + return DefaultEventPublisherOptions.builder().eventEmitInterval(step).build(); + } + + private CommandLatencyRecorder commandLatencyRecorder() { + return CommandLatencyCollector.create(DefaultCommandLatencyCollectorOptions.builder().enable().build()); + } + + public RedisURI redisURI() { + RedisURI.Builder builder = redisURIBuilder(); + if (database > 0) { + builder.withDatabase(database); + } + if (StringUtils.hasLength(clientName)) { + builder.withClientName(clientName); + } + if (!ObjectUtils.isEmpty(password)) { + if (StringUtils.hasLength(username)) { + builder.withAuthentication(username, password); + } else { + builder.withPassword(password); + } + } + if (tls) { + builder.withSsl(tls); + builder.withVerifyPeer(verifyPeer); + } + if (timeout != null) { + builder.withTimeout(timeout); + } + return builder.build(); + } + + private RedisURI.Builder redisURIBuilder() { + if (StringUtils.hasLength(uri)) { + return RedisURI.builder(RedisURI.create(uri)); + } + if (StringUtils.hasLength(socket)) { + return RedisURI.Builder.socket(socket); + } + return RedisURI.Builder.redis(host, port); + } + public String getClientName() { return clientName; } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/RedisContext.java b/core/riot-core/src/main/java/com/redis/riot/core/RedisContext.java deleted file mode 100644 index f559e6ab4..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/RedisContext.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.redis.riot.core; - -import com.redis.lettucemod.api.StatefulRedisModulesConnection; -import com.redis.lettucemod.util.RedisModulesUtils; - -import io.lettuce.core.AbstractRedisClient; -import io.lettuce.core.RedisURI; - -public class RedisContext implements AutoCloseable { - - private final RedisURI uri; - - private final AbstractRedisClient client; - - private final StatefulRedisModulesConnection connection; - - public RedisContext(RedisURI uri, AbstractRedisClient client) { - this.uri = uri; - this.client = client; - this.connection = RedisModulesUtils.connection(client); - } - - public AbstractRedisClient getClient() { - return client; - } - - public StatefulRedisModulesConnection getConnection() { - return connection; - } - - public RedisURI getUri() { - return uri; - } - - @Override - public void close() { - try { - connection.close(); - } finally { - client.close(); - client.getResources().shutdown(); - } - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/RedisReaderOptions.java b/core/riot-core/src/main/java/com/redis/riot/core/RedisReaderOptions.java index 3c7eb1978..322d6dfeb 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/RedisReaderOptions.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/RedisReaderOptions.java @@ -7,6 +7,7 @@ import com.redis.spring.batch.RedisItemReader; import com.redis.spring.batch.common.DataType; import com.redis.spring.batch.reader.AbstractKeyValueItemReader; +import com.redis.spring.batch.reader.AbstractPollableItemReader; import com.redis.spring.batch.step.FlushingChunkProvider; import io.lettuce.core.ReadFrom; @@ -14,7 +15,7 @@ public class RedisReaderOptions { public static final int DEFAULT_QUEUE_CAPACITY = RedisItemReader.DEFAULT_QUEUE_CAPACITY; - public static final Duration DEFAULT_POLL_TIMEOUT = RedisItemReader.DEFAULT_POLL_TIMEOUT; + public static final Duration DEFAULT_POLL_TIMEOUT = AbstractPollableItemReader.DEFAULT_POLL_TIMEOUT; public static final int DEFAULT_THREADS = RedisItemReader.DEFAULT_THREADS; public static final int DEFAULT_CHUNK_SIZE = RedisItemReader.DEFAULT_CHUNK_SIZE; public static final int DEFAULT_POOL_SIZE = AbstractKeyValueItemReader.DEFAULT_POOL_SIZE; @@ -23,6 +24,7 @@ public class RedisReaderOptions { public static final int DEFAULT_NOTIFICATION_QUEUE_CAPACITY = RedisItemReader.DEFAULT_NOTIFICATION_QUEUE_CAPACITY; public static final long DEFAULT_SCAN_COUNT = 1000; public static final Duration DEFAULT_FLUSH_INTERVAL = FlushingChunkProvider.DEFAULT_FLUSH_INTERVAL; + public static final Duration DEFAULT_IDLE_TIMEOUT = FlushingChunkProvider.DEFAULT_IDLE_TIMEOUT; public static final String DEFAULT_KEY_PATTERN = RedisItemReader.DEFAULT_KEY_PATTERN; private String keyPattern = DEFAULT_KEY_PATTERN; @@ -38,7 +40,7 @@ public class RedisReaderOptions { private int memoryUsageSamples = DEFAULT_MEMORY_USAGE_SAMPLES; private int notificationQueueCapacity = DEFAULT_NOTIFICATION_QUEUE_CAPACITY; private Duration flushInterval = DEFAULT_FLUSH_INTERVAL; - private Duration idleTimeout; + private Duration idleTimeout = DEFAULT_IDLE_TIMEOUT; private KeyFilterOptions keyFilterOptions = new KeyFilterOptions(); public KeyFilterOptions getKeyFilterOptions() { diff --git a/core/riot-core/src/main/java/com/redis/riot/core/RedisWriterOptions.java b/core/riot-core/src/main/java/com/redis/riot/core/RedisWriterOptions.java index 77b433ac1..fe3debe3b 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/RedisWriterOptions.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/RedisWriterOptions.java @@ -7,58 +7,53 @@ public class RedisWriterOptions { - public static final Duration DEFAULT_WAIT_TIMEOUT = AbstractOperationItemWriter.DEFAULT_WAIT_TIMEOUT; - - public static final int DEFAULT_POOL_SIZE = AbstractOperationExecutor.DEFAULT_POOL_SIZE; - - private boolean multiExec; - - private int waitReplicas; - - private Duration waitTimeout = DEFAULT_WAIT_TIMEOUT; - - private int poolSize = DEFAULT_POOL_SIZE; - - private boolean merge; - - public boolean isMerge() { - return merge; - } - - public void setMerge(boolean merge) { - this.merge = merge; - } - - public int getPoolSize() { - return poolSize; - } - - public void setPoolSize(int size) { - this.poolSize = size; - } - - public boolean isMultiExec() { - return multiExec; - } - - public void setMultiExec(boolean enable) { - this.multiExec = enable; - } - - public int getWaitReplicas() { - return waitReplicas; - } - - public void setWaitReplicas(int replicas) { - this.waitReplicas = replicas; - } - - public Duration getWaitTimeout() { - return waitTimeout; - } - - public void setWaitTimeout(Duration timeout) { - this.waitTimeout = timeout; - } + public static final Duration DEFAULT_WAIT_TIMEOUT = AbstractOperationItemWriter.DEFAULT_WAIT_TIMEOUT; + public static final int DEFAULT_POOL_SIZE = AbstractOperationExecutor.DEFAULT_POOL_SIZE; + + private boolean multiExec; + private int waitReplicas; + private Duration waitTimeout = DEFAULT_WAIT_TIMEOUT; + private int poolSize = DEFAULT_POOL_SIZE; + private boolean merge; + + public boolean isMerge() { + return merge; + } + + public void setMerge(boolean merge) { + this.merge = merge; + } + + public int getPoolSize() { + return poolSize; + } + + public void setPoolSize(int size) { + this.poolSize = size; + } + + public boolean isMultiExec() { + return multiExec; + } + + public void setMultiExec(boolean enable) { + this.multiExec = enable; + } + + public int getWaitReplicas() { + return waitReplicas; + } + + public void setWaitReplicas(int replicas) { + this.waitReplicas = replicas; + } + + public Duration getWaitTimeout() { + return waitTimeout; + } + + public void setWaitTimeout(Duration timeout) { + this.waitTimeout = timeout; + } } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/ReplicationContext.java b/core/riot-core/src/main/java/com/redis/riot/core/ReplicationContext.java deleted file mode 100644 index 326b994a1..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/ReplicationContext.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.redis.riot.core; - -public class ReplicationContext extends RiotContext { - - private final RedisContext targetRedisContext; - - public ReplicationContext(RiotContext context, RedisContext target) { - super(context.getRedisContext(), context.getEvaluationContext()); - this.targetRedisContext = target; - } - - public RedisContext getTargetRedisContext() { - return targetRedisContext; - } - - @Override - public void close() { - try { - targetRedisContext.close(); - } finally { - super.close(); - } - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/ReplicationMode.java b/core/riot-core/src/main/java/com/redis/riot/core/ReplicationMode.java deleted file mode 100644 index 343567569..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/ReplicationMode.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.redis.riot.core; - -public enum ReplicationMode { - SNAPSHOT, LIVE, LIVEONLY, COMPARE -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/ReplicationType.java b/core/riot-core/src/main/java/com/redis/riot/core/ReplicationType.java deleted file mode 100644 index 821d67b9d..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/ReplicationType.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.redis.riot.core; - -public enum ReplicationType { - DUMP, STRUCT -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/RiotContext.java b/core/riot-core/src/main/java/com/redis/riot/core/RiotContext.java deleted file mode 100644 index a838a7a69..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/RiotContext.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.redis.riot.core; - -import org.springframework.expression.spel.support.StandardEvaluationContext; - -public class RiotContext implements AutoCloseable { - - private final RedisContext redisContext; - - private final StandardEvaluationContext evaluationContext; - - public RiotContext(RedisContext redisContext, StandardEvaluationContext evaluationContext) { - this.redisContext = redisContext; - this.evaluationContext = evaluationContext; - } - - public RedisContext getRedisContext() { - return redisContext; - } - - public StandardEvaluationContext getEvaluationContext() { - return evaluationContext; - } - - @Override - public void close() { - redisContext.close(); - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/RiotException.java b/core/riot-core/src/main/java/com/redis/riot/core/RiotException.java deleted file mode 100644 index 3e5561c27..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/RiotException.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.redis.riot.core; - -public class RiotException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - public RiotException(String message) { - super(message); - } - - public RiotException(String message, Throwable cause) { - super(message, cause); - } - - public RiotException(Throwable cause) { - super(cause); - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/RiotStep.java b/core/riot-core/src/main/java/com/redis/riot/core/RiotStep.java index d2590f11a..bade32ca1 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/RiotStep.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/RiotStep.java @@ -9,55 +9,51 @@ public class RiotStep { - private String name; - - private ItemReader reader; - - private ItemProcessor processor; - - private ItemWriter writer; - + private String name; + private ItemReader reader; + private ItemProcessor processor; + private ItemWriter writer; private Consumer> configurer = b -> { - }; + }; - public String getName() { - return name; - } + public String getName() { + return name; + } - public void setName(String name) { - this.name = name; - } + public void setName(String name) { + this.name = name; + } - public ItemReader getReader() { - return reader; - } + public ItemReader getReader() { + return reader; + } - public void setReader(ItemReader reader) { - this.reader = reader; - } + public void setReader(ItemReader reader) { + this.reader = reader; + } - public ItemProcessor getProcessor() { - return processor; - } + public ItemProcessor getProcessor() { + return processor; + } - public void setProcessor(ItemProcessor processor) { - this.processor = processor; - } + public void setProcessor(ItemProcessor processor) { + this.processor = processor; + } - public ItemWriter getWriter() { - return writer; - } + public ItemWriter getWriter() { + return writer; + } - public void setWriter(ItemWriter writer) { - this.writer = writer; - } + public void setWriter(ItemWriter writer) { + this.writer = writer; + } - public Consumer> getConfigurer() { - return configurer; - } + public Consumer> getConfigurer() { + return configurer; + } - public void setConfigurer(Consumer> configurer) { - this.configurer = configurer; - } + public void setConfigurer(Consumer> configurer) { + this.configurer = configurer; + } } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/RuntimeRiotException.java b/core/riot-core/src/main/java/com/redis/riot/core/RuntimeRiotException.java deleted file mode 100644 index 7cb296ae0..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/RuntimeRiotException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.redis.riot.core; - -public class RuntimeRiotException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - public RuntimeRiotException() { - super(); - } - - public RuntimeRiotException(String message) { - super(message); - } - - public RuntimeRiotException(Throwable cause) { - super(cause); - } - - public RuntimeRiotException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/StructDiffLogger.java b/core/riot-core/src/main/java/com/redis/riot/core/StructDiffLogger.java deleted file mode 100644 index b7be42e71..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/StructDiffLogger.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.redis.riot.core; - -import java.io.PrintWriter; -import java.util.stream.StreamSupport; - -import org.springframework.batch.core.ItemWriteListener; -import org.springframework.batch.item.Chunk; - -import com.redis.spring.batch.common.KeyComparison; -import com.redis.spring.batch.common.KeyComparison.Status; - -public class StructDiffLogger implements ItemWriteListener { - - private final PrintWriter out; - - public StructDiffLogger(PrintWriter out) { - this.out = out; - } - - @Override - public void afterWrite(Chunk items) { - StreamSupport.stream(items.spliterator(), false).filter(this::notOK).map(this::toMessage).forEach(out::println); - } - - public String toMessage(KeyComparison comparison) { - switch (comparison.getStatus()) { - case MISSING: - return format("Missing key '%s'", comparison.getSource().getKey()); - case TTL: - return format("TTL mismatch on key '%s': %,d != %,d", comparison.getSource().getKey(), - comparison.getSource().getTtl(), comparison.getTarget().getTtl()); - case TYPE: - return format("Type mismatch on key '%s': %s != %s", comparison.getSource().getKey(), - comparison.getSource().getType(), comparison.getTarget().getType()); - case VALUE: - return format("Value mismatch on %s '%s'", comparison.getSource().getType(), - comparison.getSource().getKey()); - default: - return "Unknown"; - } - } - - private String format(String format, Object... args) { - return String.format(format, args); - } - - private boolean notOK(KeyComparison comparison) { - return comparison.getStatus() != Status.OK; - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/ThrottledItemWriter.java b/core/riot-core/src/main/java/com/redis/riot/core/ThrottledItemWriter.java index a0737a7c6..8c5b88258 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/ThrottledItemWriter.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/ThrottledItemWriter.java @@ -12,7 +12,6 @@ public class ThrottledItemWriter implements ItemStreamWriter { private final ItemWriter delegate; - private final long sleep; public ThrottledItemWriter(ItemWriter delegate, Duration sleep) { diff --git a/core/riot-core/src/main/java/com/redis/riot/core/function/MapToFieldFunction.java b/core/riot-core/src/main/java/com/redis/riot/core/function/MapToFieldFunction.java deleted file mode 100644 index c20da5ee3..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/function/MapToFieldFunction.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.redis.riot.core.function; - -import java.util.Map; -import java.util.function.Function; - -public class MapToFieldFunction implements Function, Object> { - - private final String key; - - private Object defaultValue = null; - - public MapToFieldFunction(String key) { - this.key = key; - } - - public void setDefaultValue(Object defaultValue) { - this.defaultValue = defaultValue; - } - - @Override - public Object apply(Map t) { - return t.getOrDefault(key, defaultValue); - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/function/MapToStringArrayFunction.java b/core/riot-core/src/main/java/com/redis/riot/core/function/MapToStringArrayFunction.java deleted file mode 100644 index 2a3d281b9..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/function/MapToStringArrayFunction.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.redis.riot.core.function; - -import java.util.Map; -import java.util.function.Function; - -public class MapToStringArrayFunction implements Function, String[]> { - - private final Function, String>[] fieldFunctions; - - public MapToStringArrayFunction(Function, String>[] fieldFunctions) { - this.fieldFunctions = fieldFunctions; - } - - @Override - public String[] apply(Map source) { - String[] array = new String[fieldFunctions.length]; - for (int index = 0; index < fieldFunctions.length; index++) { - array[index] = fieldFunctions[index].apply(source); - } - return array; - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/function/RegexNamedGroupFunction.java b/core/riot-core/src/main/java/com/redis/riot/core/function/RegexNamedGroupFunction.java index cb3ddec0c..816ffc44e 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/function/RegexNamedGroupFunction.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/function/RegexNamedGroupFunction.java @@ -11,32 +11,31 @@ public class RegexNamedGroupFunction implements Function> { - private static final String NAMED_GROUPS = "\\(\\?<([a-zA-Z][a-zA-Z0-9]*)>"; - - private final Pattern pattern; - - private final Set namedGroups; - - public RegexNamedGroupFunction(Pattern pattern) { - this.pattern = pattern; - this.namedGroups = new TreeSet<>(); - Matcher m = Pattern.compile(NAMED_GROUPS).matcher(pattern.pattern()); - while (m.find()) { - namedGroups.add(m.group(1)); - } - } - - @Override - public Map apply(String string) { - Matcher matcher = pattern.matcher(string); - if (matcher.find()) { - Map fields = new HashMap<>(); - for (String name : namedGroups) { - fields.put(name, matcher.group(name)); - } - return fields; - } - return Collections.emptyMap(); - } + private static final String NAMED_GROUPS = "\\(\\?<([a-zA-Z][a-zA-Z0-9]*)>"; + + private final Pattern pattern; + private final Set namedGroups; + + public RegexNamedGroupFunction(Pattern pattern) { + this.pattern = pattern; + this.namedGroups = new TreeSet<>(); + Matcher m = Pattern.compile(NAMED_GROUPS).matcher(pattern.pattern()); + while (m.find()) { + namedGroups.add(m.group(1)); + } + } + + @Override + public Map apply(String string) { + Matcher matcher = pattern.matcher(string); + if (matcher.find()) { + Map fields = new HashMap<>(); + for (String name : namedGroups) { + fields.put(name, matcher.group(name)); + } + return fields; + } + return Collections.emptyMap(); + } } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/function/SampleToMapFunction.java b/core/riot-core/src/main/java/com/redis/riot/core/function/SampleToMapFunction.java deleted file mode 100644 index 377bff12c..000000000 --- a/core/riot-core/src/main/java/com/redis/riot/core/function/SampleToMapFunction.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.redis.riot.core.function; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import com.redis.lettucemod.timeseries.Sample; - -public class SampleToMapFunction implements Function, Map> { - - @Override - public Map apply(List source) { - Map result = new HashMap<>(); - for (int index = 0; index < source.size(); index++) { - Sample sample = source.get(index); - result.put(String.valueOf(index), sample.getTimestamp() + ":" + sample.getValue()); - } - return result; - } - -} diff --git a/core/riot-core/src/main/java/com/redis/riot/core/function/StringToMapFunction.java b/core/riot-core/src/main/java/com/redis/riot/core/function/StringToMapFunction.java index 98442581d..7c8464bc2 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/function/StringToMapFunction.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/function/StringToMapFunction.java @@ -7,21 +7,20 @@ public class StringToMapFunction implements Function> { - public static final String DEFAULT_KEY = "value"; + public static final String DEFAULT_KEY = "value"; + public static final UnaryOperator DEFAULT_KEY_EXTRACTOR = s -> DEFAULT_KEY; - public static final UnaryOperator DEFAULT_KEY_EXTRACTOR = s -> DEFAULT_KEY; + private UnaryOperator keyExtractor = DEFAULT_KEY_EXTRACTOR; - private UnaryOperator keyExtractor = DEFAULT_KEY_EXTRACTOR; + public void setKeyExtractor(UnaryOperator keyExtractor) { + this.keyExtractor = keyExtractor; + } - public void setKeyExtractor(UnaryOperator keyExtractor) { - this.keyExtractor = keyExtractor; - } - - @Override - public Map apply(String t) { - Map result = new HashMap<>(); - result.put(keyExtractor.apply(t), t); - return result; - } + @Override + public Map apply(String t) { + Map result = new HashMap<>(); + result.put(keyExtractor.apply(t), t); + return result; + } } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/function/StructToMapFunction.java b/core/riot-core/src/main/java/com/redis/riot/core/function/StructToMapFunction.java index 90ade7a0b..c13ae16c5 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/function/StructToMapFunction.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/function/StructToMapFunction.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -17,82 +18,91 @@ public class StructToMapFunction implements Function, Map> { - private Function> key = t -> Collections.emptyMap(); - private UnaryOperator> hash = UnaryOperator.identity(); - private SampleToMapFunction timeseries = new SampleToMapFunction(); - private StreamToMapFunction stream = new StreamToMapFunction(); - private CollectionToMapFunction list = new CollectionToMapFunction(); - private CollectionToMapFunction set = new CollectionToMapFunction(); - private ZsetToMapFunction zset = new ZsetToMapFunction(); - private Function> json = new StringToMapFunction(); - private Function> string = new StringToMapFunction(); - private Function> defaultFunction = s -> null; - - public void setKey(Function> key) { - this.key = key; - } - - public void setHash(UnaryOperator> function) { - this.hash = function; - } - - public void setStream(StreamToMapFunction function) { - this.stream = function; - } - - public void setList(CollectionToMapFunction function) { - this.list = function; - } - - public void setSet(CollectionToMapFunction function) { - this.set = function; - } - - public void setZset(ZsetToMapFunction function) { - this.zset = function; - } - - public void setString(Function> function) { - this.string = function; - } - - public void setDefaultFunction(Function> function) { - this.defaultFunction = function; - } - - @Override - public Map apply(KeyValue t) { - Map map = new LinkedHashMap<>(); - map.putAll(key.apply(t.getKey())); - map.putAll(value(t.getType(), t.getValue())); - return map; - } - - @SuppressWarnings("unchecked") - private Map value(DataType type, Object value) { - if (type == null || value == null) { - return Collections.emptyMap(); - } - switch (type) { - case HASH: - return hash.apply((Map) value); - case LIST: - return list.apply((List) value); - case SET: - return set.apply((Collection) value); - case ZSET: - return zset.apply((List>) value); - case STREAM: - return stream.apply((List>) value); - case JSON: - return json.apply((String) value); - case STRING: - return string.apply((String) value); - case TIMESERIES: - return timeseries.apply((List) value); - default: - return defaultFunction.apply(value); - } - } + private Function> key = t -> Collections.emptyMap(); + private UnaryOperator> hash = UnaryOperator.identity(); + private Function, Map> timeseries = this::timeseriesToMap; + private StreamToMapFunction stream = new StreamToMapFunction(); + private CollectionToMapFunction list = new CollectionToMapFunction(); + private CollectionToMapFunction set = new CollectionToMapFunction(); + private ZsetToMapFunction zset = new ZsetToMapFunction(); + private Function> json = new StringToMapFunction(); + private Function> string = new StringToMapFunction(); + private Function> defaultFunction = s -> null; + + public void setKey(Function> key) { + this.key = key; + } + + public void setHash(UnaryOperator> function) { + this.hash = function; + } + + public void setStream(StreamToMapFunction function) { + this.stream = function; + } + + public void setList(CollectionToMapFunction function) { + this.list = function; + } + + public void setSet(CollectionToMapFunction function) { + this.set = function; + } + + public void setZset(ZsetToMapFunction function) { + this.zset = function; + } + + public void setString(Function> function) { + this.string = function; + } + + public void setDefaultFunction(Function> function) { + this.defaultFunction = function; + } + + @Override + public Map apply(KeyValue t) { + Map map = new LinkedHashMap<>(); + map.putAll(key.apply(t.getKey())); + map.putAll(value(t.getType(), t.getValue())); + return map; + } + + @SuppressWarnings("unchecked") + private Map value(DataType type, Object value) { + if (type == null || value == null) { + return Collections.emptyMap(); + } + switch (type) { + case HASH: + return hash.apply((Map) value); + case LIST: + return list.apply((List) value); + case SET: + return set.apply((Collection) value); + case ZSET: + return zset.apply((List>) value); + case STREAM: + return stream.apply((List>) value); + case JSON: + return json.apply((String) value); + case STRING: + return string.apply((String) value); + case TIMESERIES: + return timeseries.apply((List) value); + default: + return defaultFunction.apply(value); + } + } + + private Map timeseriesToMap(List source) { + Map result = new HashMap<>(); + for (int index = 0; index < source.size(); index++) { + Sample sample = source.get(index); + result.put(String.valueOf(index), sample.getTimestamp() + ":" + sample.getValue()); + } + return result; + } } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/function/ZsetToMapFunction.java b/core/riot-core/src/main/java/com/redis/riot/core/function/ZsetToMapFunction.java index c9e8ae3c4..71cc8616b 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/function/ZsetToMapFunction.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/function/ZsetToMapFunction.java @@ -9,39 +9,35 @@ public class ZsetToMapFunction implements Function>, Map> { - public static final String DEFAULT_SCORE_KEY_FORMAT = "score[%s]"; - - public static final String DEFAULT_VALUE_KEY_FORMAT = "value[%s]"; - - public static final String DEFAULT_SCORE_FORMAT = "%s"; - - private String scoreKeyFormat = DEFAULT_SCORE_KEY_FORMAT; - - private String valueKeyFormat = DEFAULT_VALUE_KEY_FORMAT; - - private String scoreFormat = DEFAULT_SCORE_FORMAT; - - public void setScoreKeyFormat(String scoreKeyFormat) { - this.scoreKeyFormat = scoreKeyFormat; - } - - public void setValueKeyFormat(String valueKeyFormat) { - this.valueKeyFormat = valueKeyFormat; - } - - public void setScoreFormat(String scoreFormat) { - this.scoreFormat = scoreFormat; - } - - @Override - public Map apply(List> source) { - Map result = new HashMap<>(); - for (int index = 0; index < source.size(); index++) { - ScoredValue scoredValue = source.get(index); - result.put(String.format(scoreKeyFormat, index), String.format(scoreFormat, scoredValue.getScore())); - result.put(String.format(valueKeyFormat, index), scoredValue.getValue()); - } - return result; - } + public static final String DEFAULT_SCORE_KEY_FORMAT = "score[%s]"; + public static final String DEFAULT_VALUE_KEY_FORMAT = "value[%s]"; + public static final String DEFAULT_SCORE_FORMAT = "%s"; + + private String scoreKeyFormat = DEFAULT_SCORE_KEY_FORMAT; + private String valueKeyFormat = DEFAULT_VALUE_KEY_FORMAT; + private String scoreFormat = DEFAULT_SCORE_FORMAT; + + public void setScoreKeyFormat(String scoreKeyFormat) { + this.scoreKeyFormat = scoreKeyFormat; + } + + public void setValueKeyFormat(String valueKeyFormat) { + this.valueKeyFormat = valueKeyFormat; + } + + public void setScoreFormat(String scoreFormat) { + this.scoreFormat = scoreFormat; + } + + @Override + public Map apply(List> source) { + Map result = new HashMap<>(); + for (int index = 0; index < source.size(); index++) { + ScoredValue scoredValue = source.get(index); + result.put(String.format(scoreKeyFormat, index), String.format(scoreFormat, scoredValue.getScore())); + result.put(String.format(valueKeyFormat, index), scoredValue.getValue()); + } + return result; + } } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/AbstractFilterMapOperationBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/AbstractFilterMapOperationBuilder.java index 6d59f06b7..20b793f06 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/AbstractFilterMapOperationBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/AbstractFilterMapOperationBuilder.java @@ -12,32 +12,31 @@ public abstract class AbstractFilterMapOperationBuilder extends AbstractMapOperationBuilder { - private List includes; - - private List excludes; - - public void setIncludes(List includes) { - this.includes = includes; - } - - public void setExcludes(List excludes) { - this.excludes = excludes; - } - - protected Function, Map> map() { - Function, Map> mapFlattener = new MapFlatteningFunction<>( - new ObjectToStringFunction()); - if (ObjectUtils.isEmpty(includes) && ObjectUtils.isEmpty(excludes)) { - return mapFlattener; - } - MapFilteringFunction filtering = new MapFilteringFunction(); - if (!ObjectUtils.isEmpty(includes)) { - filtering.includes(includes); - } - if (!ObjectUtils.isEmpty(excludes)) { - filtering.excludes(excludes); - } - return mapFlattener.andThen(filtering); - } + private List includes; + private List excludes; + + public void setIncludes(List includes) { + this.includes = includes; + } + + public void setExcludes(List excludes) { + this.excludes = excludes; + } + + protected Function, Map> map() { + Function, Map> mapFlattener = new MapFlatteningFunction<>( + new ObjectToStringFunction()); + if (ObjectUtils.isEmpty(includes) && ObjectUtils.isEmpty(excludes)) { + return mapFlattener; + } + MapFilteringFunction filtering = new MapFilteringFunction(); + if (!ObjectUtils.isEmpty(includes)) { + filtering.includes(includes); + } + if (!ObjectUtils.isEmpty(excludes)) { + filtering.excludes(excludes); + } + return mapFlattener.andThen(filtering); + } } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/AbstractMapOperationBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/AbstractMapOperationBuilder.java index a0ca4fb94..a44de79e0 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/AbstractMapOperationBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/AbstractMapOperationBuilder.java @@ -15,19 +15,13 @@ public abstract class AbstractMapOperationBuilder { public static final String DEFAULT_SEPARATOR = IdFunctionBuilder.DEFAULT_SEPARATOR; - public static final boolean DEFAULT_REMOVE_FIELDS = false; - public static final boolean DEFAULT_IGNORE_MISSING_FIELDS = false; private String keySeparator = DEFAULT_SEPARATOR; - private String keyspace; - private List keyFields; - private boolean removeFields = DEFAULT_REMOVE_FIELDS; - private boolean ignoreMissingFields = DEFAULT_IGNORE_MISSING_FIELDS; protected Function, String> toString(String field) { diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/DelBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/DelBuilder.java index d8af35fee..2f00db02c 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/DelBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/DelBuilder.java @@ -3,15 +3,13 @@ import java.util.Map; import java.util.function.Function; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.Del; public class DelBuilder extends AbstractMapOperationBuilder { @Override - protected AbstractKeyWriteOperation> operation( - Function, String> keyFunction) { - return new Del<>(keyFunction); - } + protected Del> operation(Function, String> keyFunction) { + return new Del<>(keyFunction); + } } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/ExpireAtBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/ExpireAtBuilder.java index 0c6bc59bc..fe3b79fc9 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/ExpireAtBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/ExpireAtBuilder.java @@ -3,7 +3,6 @@ import java.util.Map; import java.util.function.Function; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.ExpireAt; public class ExpireAtBuilder extends AbstractMapOperationBuilder { @@ -16,7 +15,7 @@ public ExpireAtBuilder ttl(String field) { } @Override - protected AbstractKeyWriteOperation> operation( + protected ExpireAt> operation( Function, String> keyFunction) { ExpireAt> operation = new ExpireAt<>(keyFunction); operation.setEpochFunction(toLong(ttl, 0)); diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/ExpireBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/ExpireBuilder.java index c07dd0be3..4715d558d 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/ExpireBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/ExpireBuilder.java @@ -5,7 +5,6 @@ import java.util.function.Function; import java.util.function.ToLongFunction; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.Expire; public class ExpireBuilder extends AbstractMapOperationBuilder { @@ -23,8 +22,7 @@ public void setDefaultTtl(Duration defaultTtl) { } @Override - protected AbstractKeyWriteOperation> operation( - Function, String> keyFunction) { + protected Expire> operation(Function, String> keyFunction) { Expire> operation = new Expire<>(keyFunction); operation.setTtlFunction(ttl()); return operation; diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/GeoaddBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/GeoaddBuilder.java index 4d48e22eb..a33bef0fd 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/GeoaddBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/GeoaddBuilder.java @@ -5,7 +5,6 @@ import java.util.function.ToDoubleFunction; import com.redis.spring.batch.common.ToGeoValueFunction; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.Geoadd; public class GeoaddBuilder extends AbstractCollectionMapOperationBuilder { @@ -23,8 +22,7 @@ public void setLatitude(String latitude) { } @Override - protected AbstractKeyWriteOperation> operation( - Function, String> keyFunction) { + protected Geoadd> operation(Function, String> keyFunction) { ToDoubleFunction> lon = toDouble(longitude, 0); ToDoubleFunction> lat = toDouble(latitude, 0); ToGeoValueFunction> valueFunction = new ToGeoValueFunction<>(member(), lon, lat); diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/HsetBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/HsetBuilder.java index 1617476cf..32d49fc9e 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/HsetBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/HsetBuilder.java @@ -3,14 +3,12 @@ import java.util.Map; import java.util.function.Function; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.Hset; public class HsetBuilder extends AbstractFilterMapOperationBuilder { @Override - protected AbstractKeyWriteOperation> operation( - Function, String> keyFunction) { + protected Hset> operation(Function, String> keyFunction) { return new Hset<>(keyFunction, map()); } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/JsonSetBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/JsonSetBuilder.java index 6a73df77f..eeb8bf182 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/JsonSetBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/JsonSetBuilder.java @@ -8,7 +8,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.JsonSet; public class JsonSetBuilder extends AbstractMapOperationBuilder { @@ -22,7 +21,7 @@ public void setPath(String path) { } @Override - protected AbstractKeyWriteOperation> operation( + protected JsonSet> operation( Function, String> keyFunction) { JsonSet> operation = new JsonSet<>(keyFunction, this::value); operation.setPathFunction(path()); diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/LpushBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/LpushBuilder.java index 69a2803c0..fa92b6a56 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/LpushBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/LpushBuilder.java @@ -3,14 +3,12 @@ import java.util.Map; import java.util.function.Function; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.Lpush; public class LpushBuilder extends AbstractCollectionMapOperationBuilder { @Override - protected AbstractKeyWriteOperation> operation( - Function, String> keyFunction) { + protected Lpush> operation(Function, String> keyFunction) { Lpush> operation = new Lpush<>(keyFunction); operation.setValueFunction(member()); return operation; diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/RpushBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/RpushBuilder.java index bd9a2ef27..c6091d9d7 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/RpushBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/RpushBuilder.java @@ -3,14 +3,12 @@ import java.util.Map; import java.util.function.Function; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.Rpush; public class RpushBuilder extends AbstractCollectionMapOperationBuilder { @Override - protected AbstractKeyWriteOperation> operation( - Function, String> keyFunction) { + protected Rpush> operation(Function, String> keyFunction) { Rpush> operation = new Rpush<>(keyFunction); operation.setValueFunction(member()); return operation; diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/SaddBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/SaddBuilder.java index 3e41acd6a..45faf3cb1 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/SaddBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/SaddBuilder.java @@ -3,14 +3,12 @@ import java.util.Map; import java.util.function.Function; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.Sadd; public class SaddBuilder extends AbstractCollectionMapOperationBuilder { @Override - protected AbstractKeyWriteOperation> operation( - Function, String> keyFunction) { + protected Sadd> operation(Function, String> keyFunction) { return new Sadd<>(keyFunction, member()); } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/SetBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/SetBuilder.java index c9c4f1c7c..87397e430 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/SetBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/SetBuilder.java @@ -8,7 +8,6 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.redis.riot.core.function.ObjectMapperFunction; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.Set; public class SetBuilder extends AbstractMapOperationBuilder { @@ -20,9 +19,7 @@ public enum StringFormat { public static final StringFormat DEFAULT_FORMAT = StringFormat.JSON; private StringFormat format = DEFAULT_FORMAT; - private String field; - private String root; public void setFormat(StringFormat format) { @@ -38,8 +35,7 @@ public void setRoot(String root) { } @Override - protected AbstractKeyWriteOperation> operation( - Function, String> keyFunction) { + protected Set> operation(Function, String> keyFunction) { return new Set<>(keyFunction, value()); } diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/SugaddBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/SugaddBuilder.java index 4c89cd2e3..65d89d241 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/SugaddBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/SugaddBuilder.java @@ -6,23 +6,17 @@ import com.redis.lettucemod.search.Suggestion; import com.redis.spring.batch.common.ToSuggestionFunction; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.Sugadd; public class SugaddBuilder extends AbstractMapOperationBuilder { public static final double DEFAULT_SCORE = 1; - public static final boolean DEFAULT_INCREMENT = false; private String stringField; - private String scoreField; - private double defaultScore = DEFAULT_SCORE; - private String payloadField; - private boolean increment = DEFAULT_INCREMENT; public void setStringField(String stringField) { @@ -46,8 +40,7 @@ public void setIncrement(boolean increment) { } @Override - protected AbstractKeyWriteOperation> operation( - Function, String> keyFunction) { + protected Sugadd> operation(Function, String> keyFunction) { Sugadd> operation = new Sugadd<>(keyFunction, suggestion()); operation.setIncr(increment); return operation; diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/TsAddBuilder.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/TsAddBuilder.java index dcddb1e6e..e094cae42 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/TsAddBuilder.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/TsAddBuilder.java @@ -16,7 +16,6 @@ import com.redis.lettucemod.timeseries.Label; import com.redis.lettucemod.timeseries.Sample; import com.redis.spring.batch.common.ToSampleFunction; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.TsAdd; public class TsAddBuilder extends AbstractMapOperationBuilder { @@ -32,8 +31,7 @@ public class TsAddBuilder extends AbstractMapOperationBuilder { private Map labels; @Override - protected AbstractKeyWriteOperation> operation( - Function, String> keyFunction) { + protected TsAdd> operation(Function, String> keyFunction) { TsAdd> operation = new TsAdd<>(keyFunction, sample()); operation.setOptionsFunction(this::addOptions); return operation; diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/XaddSupplier.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/XaddSupplier.java index b523dd216..2c1cc73f0 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/XaddSupplier.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/XaddSupplier.java @@ -3,7 +3,6 @@ import java.util.Map; import java.util.function.Function; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.Xadd; import io.lettuce.core.XAddArgs; @@ -11,7 +10,6 @@ public class XaddSupplier extends AbstractFilterMapOperationBuilder { private long maxlen; - private boolean approximateTrimming; public void setMaxlen(long maxlen) { @@ -23,8 +21,7 @@ public void setApproximateTrimming(boolean approximateTrimming) { } @Override - protected AbstractKeyWriteOperation> operation( - Function, String> keyFunction) { + protected Xadd> operation(Function, String> keyFunction) { Xadd> operation = new Xadd<>(keyFunction, map()); operation.setArgs(args()); return operation; diff --git a/core/riot-core/src/main/java/com/redis/riot/core/operation/ZaddSupplier.java b/core/riot-core/src/main/java/com/redis/riot/core/operation/ZaddSupplier.java index 653a22c9a..d86924b08 100644 --- a/core/riot-core/src/main/java/com/redis/riot/core/operation/ZaddSupplier.java +++ b/core/riot-core/src/main/java/com/redis/riot/core/operation/ZaddSupplier.java @@ -5,7 +5,6 @@ import java.util.function.ToDoubleFunction; import com.redis.spring.batch.common.ToScoredValueFunction; -import com.redis.spring.batch.writer.operation.AbstractKeyWriteOperation; import com.redis.spring.batch.writer.operation.Zadd; public class ZaddSupplier extends AbstractCollectionMapOperationBuilder { @@ -17,8 +16,7 @@ public class ZaddSupplier extends AbstractCollectionMapOperationBuilder { private double defaultScore = DEFAULT_SCORE; @Override - protected AbstractKeyWriteOperation> operation( - Function, String> keyFunction) { + protected Zadd> operation(Function, String> keyFunction) { return new Zadd<>(keyFunction, value()); } diff --git a/core/riot-core/src/test/java/com/redis/riot/core/test/ConverterTests.java b/core/riot-core/src/test/java/com/redis/riot/core/ConverterTests.java similarity index 98% rename from core/riot-core/src/test/java/com/redis/riot/core/test/ConverterTests.java rename to core/riot-core/src/test/java/com/redis/riot/core/ConverterTests.java index 5ca692b52..7a436f18a 100644 --- a/core/riot-core/src/test/java/com/redis/riot/core/test/ConverterTests.java +++ b/core/riot-core/src/test/java/com/redis/riot/core/ConverterTests.java @@ -1,4 +1,4 @@ -package com.redis.riot.core.test; +package com.redis.riot.core; import java.util.HashMap; import java.util.Map; diff --git a/core/riot-core/src/test/java/com/redis/riot/core/test/FunctionTests.java b/core/riot-core/src/test/java/com/redis/riot/core/FunctionTests.java similarity index 97% rename from core/riot-core/src/test/java/com/redis/riot/core/test/FunctionTests.java rename to core/riot-core/src/test/java/com/redis/riot/core/FunctionTests.java index 92b4da1da..8dc09f2ea 100644 --- a/core/riot-core/src/test/java/com/redis/riot/core/test/FunctionTests.java +++ b/core/riot-core/src/test/java/com/redis/riot/core/FunctionTests.java @@ -1,4 +1,4 @@ -package com.redis.riot.core.test; +package com.redis.riot.core; import java.util.HashMap; import java.util.Map; diff --git a/core/riot-core/src/test/java/com/redis/riot/core/ProcessorTests.java b/core/riot-core/src/test/java/com/redis/riot/core/ProcessorTests.java new file mode 100644 index 000000000..3486ebdca --- /dev/null +++ b/core/riot-core/src/test/java/com/redis/riot/core/ProcessorTests.java @@ -0,0 +1,81 @@ +package com.redis.riot.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +class ProcessorTests { + + @Test + void keyFilter() { + KeyFilterOptions options = new KeyFilterOptions(); + options.setIncludes(Arrays.asList("foo*", "bar*")); + Predicate predicate = RiotUtils.keyFilterPredicate(options); + Assertions.assertTrue(predicate.test("foobar")); + Assertions.assertTrue(predicate.test("barfoo")); + Assertions.assertFalse(predicate.test("key")); + } + + @Test + void testMapProcessor() throws Exception { + Map expressions = new LinkedHashMap<>(); + expressions.put("field1", RiotUtils.parse("'test:1'")); + ImportProcessorOptions options = new ImportProcessorOptions(); + options.setProcessorExpressions(expressions); + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + ItemProcessor, Map> processor = options.processor(evaluationContext); + Map map = processor.process(new HashMap<>()); + Assertions.assertEquals("test:1", map.get("field1")); + // Assertions.assertEquals("1", map.get("id")); + } + + @Test + void processor() throws Exception { + Map expressions = new LinkedHashMap<>(); + expressions.put("field1", RiotUtils.parse("'value1'")); + expressions.put("field2", RiotUtils.parse("field1")); + expressions.put("field3", RiotUtils.parse("1")); + expressions.put("field4", RiotUtils.parse("2")); + expressions.put("field5", RiotUtils.parse("field3+field4")); + ImportProcessorOptions options = new ImportProcessorOptions(); + options.setProcessorExpressions(expressions); + ItemProcessor, Map> processor = options + .processor(new StandardEvaluationContext()); + for (int index = 0; index < 10; index++) { + Map result = processor.process(new HashMap<>()); + assertEquals(5, result.size()); + assertEquals("value1", result.get("field1")); + assertEquals("value1", result.get("field2")); + assertEquals(3, result.get("field5")); + } + } + + @Test + void processorFilter() throws Exception { + ImportProcessorOptions options = new ImportProcessorOptions(); + options.setFilterExpression(RiotUtils.parse("index<10")); + ItemProcessor, Map> processor = options + .processor(new StandardEvaluationContext()); + for (int index = 0; index < 100; index++) { + Map map = new HashMap<>(); + map.put("index", index); + Map result = processor.process(map); + if (index < 10) { + Assertions.assertNotNull(result); + } else { + Assertions.assertNull(result); + } + } + } + +} diff --git a/core/riot-core/src/test/java/com/redis/riot/core/test/JsonSerdeTests.java b/core/riot-core/src/test/java/com/redis/riot/core/test/JsonSerdeTests.java deleted file mode 100644 index 24bb79412..000000000 --- a/core/riot-core/src/test/java/com/redis/riot/core/test/JsonSerdeTests.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.redis.riot.core.test; - -import java.time.Instant; -import java.util.Arrays; -import java.util.List; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestInstance.Lifecycle; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.util.unit.DataSize; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.DoubleNode; -import com.redis.lettucemod.timeseries.Sample; -import com.redis.riot.core.KeyValueDeserializer; -import com.redis.spring.batch.common.DataType; -import com.redis.spring.batch.common.KeyValue; -import com.redis.spring.batch.gen.GeneratorItemReader; -import com.redis.spring.batch.test.AbstractTestBase; - -@TestInstance(Lifecycle.PER_CLASS) -class JsonSerdeTests { - - private static final String timeseries = "{\"key\":\"gen:97\",\"type\":\"timeseries\",\"value\":[{\"timestamp\":1695939533285,\"value\":0.07027403662738285},{\"timestamp\":1695939533286,\"value\":0.7434808603018632},{\"timestamp\":1695939533287,\"value\":0.36481049906367213},{\"timestamp\":1695939533288,\"value\":0.08986928499552382},{\"timestamp\":1695939533289,\"value\":0.3901401870373925},{\"timestamp\":1695939533290,\"value\":0.1088584873055678},{\"timestamp\":1695939533291,\"value\":0.5649631025302376},{\"timestamp\":1695939533292,\"value\":0.9284983053028953},{\"timestamp\":1695939533293,\"value\":0.5009349293022067},{\"timestamp\":1695939533294,\"value\":0.7798297389022721}],\"ttl\":-1,\"memoryUsage\":0}"; - - private ObjectMapper mapper = new ObjectMapper(); - - @BeforeAll - void setup() { - mapper.configure(DeserializationFeature.USE_LONG_FOR_INTS, true); - SimpleModule module = new SimpleModule(); - module.addDeserializer(KeyValue.class, new KeyValueDeserializer()); - mapper.registerModule(module); - } - - @SuppressWarnings("unchecked") - @Test - void deserialize() throws JsonMappingException, JsonProcessingException { - KeyValue keyValue = mapper.readValue(timeseries, KeyValue.class); - Assertions.assertEquals("gen:97", keyValue.getKey()); - } - - @Test - void serialize() throws JsonProcessingException { - String key = "ts:1"; - long memoryUsage = DataSize.ofGigabytes(1).toBytes(); - long ttl = Instant.now().toEpochMilli(); - KeyValue ts = new KeyValue<>(); - ts.setKey(key); - ts.setMemoryUsage(memoryUsage); - ts.setTtl(ttl); - ts.setType(DataType.TIMESERIES); - Sample sample1 = Sample.of(Instant.now().toEpochMilli(), 123.456); - Sample sample2 = Sample.of(Instant.now().toEpochMilli() + 1000, 456.123); - ts.setValue(Arrays.asList(sample1, sample2)); - String json = mapper.writeValueAsString(ts); - JsonNode jsonNode = mapper.readTree(json); - Assertions.assertEquals(key, jsonNode.get("key").asText()); - ArrayNode valueNode = (ArrayNode) jsonNode.get("value"); - Assertions.assertEquals(2, valueNode.size()); - Assertions.assertEquals(sample2.getValue(), ((DoubleNode) valueNode.get(1).get("value")).asDouble()); - } - - @SuppressWarnings("unchecked") - @Test - void serde() throws Exception { - GeneratorItemReader reader = new GeneratorItemReader(); - reader.setMaxItemCount(17); - reader.open(new ExecutionContext()); - List> items = AbstractTestBase.readAll(reader); - for (KeyValue item : items) { - String json = mapper.writeValueAsString(item); - KeyValue result = mapper.readValue(json, KeyValue.class); - System.out.println(item.getType()); - Assertions.assertEquals(item, result); - } - } - -} diff --git a/core/riot-core/src/test/java/com/redis/riot/core/test/ProcessorTests.java b/core/riot-core/src/test/java/com/redis/riot/core/test/ProcessorTests.java deleted file mode 100644 index c41a4db6e..000000000 --- a/core/riot-core/src/test/java/com/redis/riot/core/test/ProcessorTests.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.redis.riot.core.test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.function.Predicate; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.springframework.batch.core.Job; -import org.springframework.batch.item.ItemProcessor; -import org.springframework.expression.Expression; -import org.springframework.expression.spel.support.StandardEvaluationContext; - -import com.redis.riot.core.AbstractImport; -import com.redis.riot.core.KeyFilterOptions; -import com.redis.riot.core.RiotContext; -import com.redis.riot.core.RiotUtils; - -class ProcessorTests { - - @Test - void keyFilter() { - KeyFilterOptions options = new KeyFilterOptions(); - options.setIncludes(Arrays.asList("foo*", "bar*")); - Predicate predicate = RiotUtils.keyFilterPredicate(options); - Assertions.assertTrue(predicate.test("foobar")); - Assertions.assertTrue(predicate.test("barfoo")); - Assertions.assertFalse(predicate.test("key")); - } - - @Test - void testMapProcessor() throws Exception { - DummyMapImport mapImport = new DummyMapImport(); - Map expressions = new LinkedHashMap<>(); - expressions.put("field1", RiotUtils.parse("'test:1'")); - mapImport.setProcessorExpressions(expressions); - StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); - ItemProcessor, Map> processor = mapImport.processor(evaluationContext); - Map map = processor.process(new HashMap<>()); - Assertions.assertEquals("test:1", map.get("field1")); - // Assertions.assertEquals("1", map.get("id")); - } - - private static class DummyMapImport extends AbstractImport { - - @Override - protected Job job(RiotContext executionContext) { - return null; - } - - } - - @Test - void processor() throws Exception { - DummyMapImport mapImport = new DummyMapImport(); - Map expressions = new LinkedHashMap<>(); - expressions.put("field1", RiotUtils.parse("'value1'")); - expressions.put("field2", RiotUtils.parse("field1")); - expressions.put("field3", RiotUtils.parse("1")); - expressions.put("field4", RiotUtils.parse("2")); - expressions.put("field5", RiotUtils.parse("field3+field4")); - mapImport.setProcessorExpressions(expressions); - ItemProcessor, Map> processor = mapImport - .processor(new StandardEvaluationContext()); - for (int index = 0; index < 10; index++) { - Map result = processor.process(new HashMap<>()); - assertEquals(5, result.size()); - assertEquals("value1", result.get("field1")); - assertEquals("value1", result.get("field2")); - assertEquals(3, result.get("field5")); - } - } - - @Test - void processorFilter() throws Exception { - DummyMapImport mapImport = new DummyMapImport(); - mapImport.setFilterExpression(RiotUtils.parse("index<10")); - ItemProcessor, Map> processor = mapImport - .processor(new StandardEvaluationContext()); - for (int index = 0; index < 100; index++) { - Map map = new HashMap<>(); - map.put("index", index); - Map result = processor.process(map); - if (index < 10) { - Assertions.assertNotNull(result); - } else { - Assertions.assertNull(result); - } - } - } - -} diff --git a/core/riot-core/src/test/java/com/redis/riot/core/test/StackTests.java b/core/riot-core/src/test/java/com/redis/riot/core/test/StackTests.java deleted file mode 100644 index f4f0bf88b..000000000 --- a/core/riot-core/src/test/java/com/redis/riot/core/test/StackTests.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.redis.riot.core.test; - -import com.redis.spring.batch.common.DataType; -import com.redis.spring.batch.test.AbstractTestBase; -import com.redis.testcontainers.RedisStackContainer; - -class StackTests extends AbstractReplicationTests { - - public static final RedisStackContainer SOURCE = RedisContainerFactory.stack(); - - public static final RedisStackContainer TARGET = RedisContainerFactory.stack(); - - @Override - protected RedisStackContainer getRedisServer() { - return SOURCE; - } - - @Override - protected RedisStackContainer getTargetRedisServer() { - return TARGET; - } - - @Override - protected DataType[] generatorDataTypes() { - return AbstractTestBase.REDIS_MODULES_GENERATOR_TYPES; - } - -} diff --git a/docs/guide/src/docs/asciidoc/replication.adoc b/docs/guide/src/docs/asciidoc/replication.adoc index 552d23d81..73f89691c 100644 --- a/docs/guide/src/docs/asciidoc/replication.adoc +++ b/docs/guide/src/docs/asciidoc/replication.adoc @@ -28,7 +28,7 @@ The basic replication mechanism is as follows: [source] ---- -riot replicate --mode --type [OPTIONS] +riot replicate --mode [--type] [OPTIONS] ---- For the full usage, run: diff --git a/gradle.properties b/gradle.properties index 973375ec1..6bb97f867 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,46 +25,20 @@ reproducibleBuild = true bootPluginVersion = 3.2.3 dependencyPluginVersion = 1.1.4 gitPluginVersion = 3.0.0 +jacocoVersion = 0.8.11 kordampBuildVersion = 3.4.0 kordampPluginVersion = 0.54.0 -awaitilityVersion = 4.2.0 awsVersion = 2.2.6.RELEASE -commonsIoVersion = 2.14.0 -commonsPoolVersion = 2.12.0 -db2Version = 11.5.8.0 -datafakerVersion = 2.0.2 -errorproneVersion = 2.4.0 +datafakerVersion = 2.1.0 gcpVersion = 1.2.8.RELEASE -guavaVersion = 30.0-android -hdrVersion = 2.1.12 -hsqldbVersion = 2.7.2 -jacksonVersion = 2.15.3 -jacocoVersion = 0.8.11 -jdksPluginVersion = 1.5.0 -jsr305Version = 3.0.2 -junitVersion = 5.10.1 -junitPlatformVersion = 1.10.1 +jdksPluginVersion = 1.5.1 latencyutilsVersion = 2.0.3 lettucemodVersion = 3.7.3 -lettuceVersion = 6.2.6.RELEASE -mssqlVersion = 12.4.2.jre8 -mysqlVersion = 8.1.0 -oracleVersion = 19.3.0.0 picocliVersion = 4.7.5 -plexusVersion = 4.0.0 -postgresqlVersion = 42.7.1 progressbarVersion = 0.10.0 -protobufVersion = 3.21.9 -slf4jVersion = 2.0.9 -snakeyamlVersion = 2.2 -springBatchRedisVersion = 4.0.2 -springBatchVersion = 5.1.0 -springBootVersion = 3.2.2 -springVersion = 6.1.1 -sqliteVersion = 3.43.0.0 +springBatchRedisVersion = 4.0.4 testcontainersRedisVersion = 2.2.0 -testcontainersVersion = 1.19.5 org.gradle.daemon = false org.gradle.caching = false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49b765f8051ef9d0a6055ff8e46073d8..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 63721 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鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fa8f862f..a80b22ce5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 6689b85be..7101f8e46 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/plugins/riot/riot.gradle b/plugins/riot/riot.gradle index 95849cdd7..19cfb5b3e 100644 --- a/plugins/riot/riot.gradle +++ b/plugins/riot/riot.gradle @@ -23,10 +23,16 @@ plugins { application { applicationName = 'riot' - mainClassName = 'com.redis.riot.cli.Main' + mainClass = 'com.redis.riot.cli.Main' +} + +bootJar { + enabled = false } jar { + enabled = true + archiveClassifier = '' manifest { attributes([ 'Main-Class': 'com.redis.riot.cli.Main', @@ -41,7 +47,6 @@ startScripts { config { info { - bytecodeVersion = 8 specification { enabled = true } } licensing { @@ -56,21 +61,22 @@ dependencies { api(project(':riot-file')) { exclude group: 'commons-logging', module: 'commons-logging' } + api project(':riot-redis') api group: 'info.picocli', name: 'picocli', version: picocliVersion annotationProcessor group: 'info.picocli', name: 'picocli-codegen', version: picocliVersion implementation group: 'me.tongfei', name: 'progressbar', version: progressbarVersion - implementation group: 'org.codehaus.plexus', name: 'plexus-utils', version: plexusVersion - implementation group: 'org.slf4j', name: 'slf4j-simple', version: slf4jVersion - implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: jacksonVersion - implementation group: 'com.mysql', name: 'mysql-connector-j', version: mysqlVersion - implementation group: 'org.postgresql', name: 'postgresql', version: postgresqlVersion - implementation group: 'org.springframework', name: 'spring-oxm', version: springVersion + implementation 'org.slf4j:slf4j-simple' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' + implementation 'com.mysql:mysql-connector-j' + implementation 'org.postgresql:postgresql' + implementation 'org.springframework:spring-oxm' + testImplementation 'org.springframework.boot:spring-boot-starter-jdbc' testImplementation group: 'com.redis', name: 'spring-batch-redis', version: springBatchRedisVersion, classifier: 'tests' - testImplementation group: 'org.awaitility', name: 'awaitility', version: awaitilityVersion - testImplementation group: 'org.testcontainers', name: 'postgresql', version: testcontainersVersion - testImplementation group: 'org.testcontainers', name: 'oracle-xe', version: testcontainersVersion - testImplementation group: 'org.springframework.boot', name: 'spring-boot-autoconfigure', version: springBootVersion - testImplementation group: 'org.hsqldb', name: 'hsqldb', version: hsqldbVersion + testImplementation 'org.awaitility:awaitility' + testImplementation 'org.testcontainers:postgresql' + testImplementation 'org.testcontainers:oracle-xe' + testImplementation 'org.springframework.boot:spring-boot-autoconfigure' + testImplementation 'org.hsqldb:hsqldb' } distributions { @@ -127,13 +133,16 @@ jdks { } } -copyDependencies { - dependsOn classes - inputs.files(configurations.runtimeClasspath) - configuration = 'runtimeClasspath' -} +bootStartScripts.dependsOn jar -assemble.dependsOn copyDependencies +afterEvaluate { + def copyJdksToCache = project.tasks.findByName('copyJdksToCache') + ['zulu17Linux', 'zulu17LinuxArm', 'zulu17LinuxMusl', 'zulu17LinuxMuslArm', + 'zulu17Windows', 'zulu17WindowsArm', 'zulu17Osx', 'zulu17OsxArm'].each { jdk -> + def copyTask = project.tasks.findByName('copyJdkFromCache' + jdk.capitalize()) + if (copyJdksToCache && copyTask) copyTask.dependsOn(copyJdksToCache) + } +} mainClassName = "com.redis.riot.cli.Main" diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractExportCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractExportCommand.java index 6ed018aab..42fe71cd2 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractExportCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractExportCommand.java @@ -19,14 +19,14 @@ public abstract class AbstractExportCommand extends AbstractJobCommand { KeyValueProcessorArgs processorArgs = new KeyValueProcessorArgs(); @Override - protected AbstractJobRunnable jobExecutable() { - AbstractExport export = exportExecutable(); + protected AbstractJobRunnable jobRunnable() { + AbstractExport export = exportRunnable(); export.setReaderOptions(readerArgs.readerOptions()); export.setProcessorOptions(processorArgs.processorOptions()); return export; } - protected abstract AbstractExport exportExecutable(); + protected abstract AbstractExport exportRunnable(); @Override protected Callable initialMaxSupplier(RiotStep step) { diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractImportCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractImportCommand.java index 390135c40..233f976e7 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractImportCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractImportCommand.java @@ -1,51 +1,32 @@ package com.redis.riot.cli; -import java.time.Duration; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.springframework.expression.Expression; -import com.redis.lettucemod.timeseries.DuplicatePolicy; -import com.redis.riot.cli.AbstractImportCommand.DelCommand; -import com.redis.riot.cli.AbstractImportCommand.ExpireCommand; -import com.redis.riot.cli.AbstractImportCommand.GeoaddCommand; -import com.redis.riot.cli.AbstractImportCommand.HsetCommand; -import com.redis.riot.cli.AbstractImportCommand.JsonSetCommand; -import com.redis.riot.cli.AbstractImportCommand.LpushCommand; -import com.redis.riot.cli.AbstractImportCommand.RpushCommand; -import com.redis.riot.cli.AbstractImportCommand.SaddCommand; -import com.redis.riot.cli.AbstractImportCommand.SetCommand; -import com.redis.riot.cli.AbstractImportCommand.SugaddCommand; -import com.redis.riot.cli.AbstractImportCommand.TsAddCommand; -import com.redis.riot.cli.AbstractImportCommand.XaddCommand; -import com.redis.riot.cli.AbstractImportCommand.ZaddCommand; +import com.redis.riot.cli.redis.DelCommand; +import com.redis.riot.cli.redis.ExpireCommand; +import com.redis.riot.cli.redis.GeoaddCommand; +import com.redis.riot.cli.redis.HsetCommand; +import com.redis.riot.cli.redis.JsonSetCommand; +import com.redis.riot.cli.redis.LpushCommand; +import com.redis.riot.cli.redis.RpushCommand; +import com.redis.riot.cli.redis.SaddCommand; +import com.redis.riot.cli.redis.SetCommand; +import com.redis.riot.cli.redis.SugaddCommand; +import com.redis.riot.cli.redis.TsAddCommand; +import com.redis.riot.cli.redis.XaddCommand; +import com.redis.riot.cli.redis.ZaddCommand; import com.redis.riot.core.AbstractImport; +import com.redis.riot.core.ImportProcessorOptions; import com.redis.riot.core.RiotStep; -import com.redis.riot.core.operation.AbstractCollectionMapOperationBuilder; -import com.redis.riot.core.operation.AbstractFilterMapOperationBuilder; -import com.redis.riot.core.operation.AbstractMapOperationBuilder; -import com.redis.riot.core.operation.DelBuilder; -import com.redis.riot.core.operation.ExpireBuilder; -import com.redis.riot.core.operation.GeoaddBuilder; -import com.redis.riot.core.operation.HsetBuilder; -import com.redis.riot.core.operation.JsonSetBuilder; -import com.redis.riot.core.operation.LpushBuilder; -import com.redis.riot.core.operation.RpushBuilder; -import com.redis.riot.core.operation.SaddBuilder; -import com.redis.riot.core.operation.SetBuilder; -import com.redis.riot.core.operation.SetBuilder.StringFormat; -import com.redis.riot.core.operation.SugaddBuilder; -import com.redis.riot.core.operation.TsAddBuilder; -import com.redis.riot.core.operation.XaddSupplier; -import com.redis.riot.core.operation.ZaddSupplier; import com.redis.spring.batch.common.Operation; +import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; -import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; @Command(subcommands = { ExpireCommand.class, DelCommand.class, GeoaddCommand.class, HsetCommand.class, @@ -60,408 +41,47 @@ public abstract class AbstractImportCommand extends AbstractJobCommand { @Option(names = "--filter", description = "Discard records using a SpEL expression.", paramLabel = "") Expression filter; + @ArgGroup(exclusive = false) + EvaluationContextArgs evaluationContextArgs = new EvaluationContextArgs(); + /** * Initialized manually during command parsing */ - private List commands = new ArrayList<>(); + private List commands = new ArrayList<>(); - public List getCommands() { + public List getCommands() { return commands; } - public void setCommands(List commands) { + public void setCommands(List commands) { this.commands = commands; } protected List, Object>> operations() { - return commands.stream().map(OperationCommand::operation).collect(Collectors.toList()); + return commands.stream().map(RedisCommand::operation).collect(Collectors.toList()); } @Override - protected AbstractImport jobExecutable() { - AbstractImport executable = importExecutable(); - executable.setOperations(operations()); - executable.setProcessorExpressions(processorExpressions); - executable.setFilterExpression(filter); - return executable; + protected AbstractImport jobRunnable() { + AbstractImport runnable = importRunnable(); + runnable.setOperations(operations()); + runnable.setEvaluationContextOptions(evaluationContextArgs.evaluationContextOptions()); + runnable.setProcessorOptions(processorOptions()); + return runnable; + } + + private ImportProcessorOptions processorOptions() { + ImportProcessorOptions options = new ImportProcessorOptions(); + options.setProcessorExpressions(processorExpressions); + options.setFilterExpression(filter); + return options; } - protected abstract AbstractImport importExecutable(); + protected abstract AbstractImport importRunnable(); @Override protected String taskName(RiotStep step) { return "Importing"; } - protected static class FieldFilteringArgs { - - @Option(arity = "1..*", names = "--include", description = "Fields to include.", paramLabel = "") - List includes; - - @Option(arity = "1..*", names = "--exclude", description = "Fields to exclude.", paramLabel = "") - List excludes; - - public void configure(AbstractFilterMapOperationBuilder builder) { - builder.setIncludes(includes); - builder.setExcludes(excludes); - } - - } - - @Command(name = "del", description = "Delete keys") - public static class DelCommand extends OperationCommand { - - @Override - protected DelBuilder operationBuilder() { - return new DelBuilder(); - } - - } - - @Command(name = "expire", description = "Set timeouts on keys") - public static class ExpireCommand extends OperationCommand { - - public static final long DEFAULT_TTL = 60; - - @Option(names = "--ttl", description = "EXPIRE timeout field.", paramLabel = "") - private String ttlField; - - @Option(names = "--ttl-default", description = "EXPIRE default timeout (default: ${DEFAULT-VALUE}).", paramLabel = "") - private long defaultTtl = DEFAULT_TTL; - - @Override - protected ExpireBuilder operationBuilder() { - ExpireBuilder builder = new ExpireBuilder(); - builder.setTtlField(ttlField); - builder.setDefaultTtl(Duration.ofSeconds(defaultTtl)); - return builder; - } - - } - - @Command(name = "geoadd", description = "Add members to a geo set") - public static class GeoaddCommand extends AbstractCollectionCommand { - - @Option(names = "--lon", required = true, description = "Longitude field.", paramLabel = "") - private String longitude; - - @Option(names = "--lat", required = true, description = "Latitude field.", paramLabel = "") - private String latitude; - - @Override - protected GeoaddBuilder collectionOperationBuilder() { - GeoaddBuilder builder = new GeoaddBuilder(); - builder.setLatitude(latitude); - builder.setLongitude(longitude); - return builder; - } - - } - - @Command(name = "hset", aliases = "hmset", description = "Set hashes from input") - public static class HsetCommand extends OperationCommand { - - @Mixin - private FieldFilteringArgs filteringArgs = new FieldFilteringArgs(); - - @Override - protected HsetBuilder operationBuilder() { - HsetBuilder builder = new HsetBuilder(); - filteringArgs.configure(builder); - return builder; - } - - } - - @Command(name = "json.set", description = "Add JSON documents to RedisJSON") - public static class JsonSetCommand extends OperationCommand { - - @Option(names = "--path", description = "Path field.", paramLabel = "") - private String path; - - @Override - protected JsonSetBuilder operationBuilder() { - JsonSetBuilder supplier = new JsonSetBuilder(); - supplier.setPath(path); - return supplier; - } - - } - - @Command(name = "lpush", description = "Insert values at the head of a list") - public static class LpushCommand extends AbstractCollectionCommand { - - @Override - protected LpushBuilder collectionOperationBuilder() { - return new LpushBuilder(); - } - - } - - @Command(name = "rpush", description = "Insert values at the tail of a list") - public static class RpushCommand extends AbstractCollectionCommand { - - @Override - protected RpushBuilder collectionOperationBuilder() { - return new RpushBuilder(); - } - - } - - @Command(name = "sadd", description = "Add members to a set") - public static class SaddCommand extends AbstractCollectionCommand { - - @Override - protected SaddBuilder collectionOperationBuilder() { - return new SaddBuilder(); - } - - } - - @Command(name = "set", description = "Set strings from input") - public static class SetCommand extends OperationCommand { - - public static final StringFormat DEFAULT_FORMAT = StringFormat.JSON; - - @Option(names = "--format", description = "Serialization: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE}).", paramLabel = "") - private StringFormat format = DEFAULT_FORMAT; - - @Option(names = "--field", description = "Raw value field.", paramLabel = "") - private String field; - - @Option(names = "--root", description = "XML root element name.", paramLabel = "") - private String root; - - @Override - protected SetBuilder operationBuilder() { - SetBuilder supplier = new SetBuilder(); - supplier.setField(field); - supplier.setFormat(format); - supplier.setRoot(root); - return supplier; - } - - } - - @Command(name = "ft.sugadd", description = "Add suggestion strings to a RediSearch auto-complete dictionary") - public static class SugaddCommand extends OperationCommand { - - public static final double DEFAULT_SCORE = 1; - - public static final boolean DEFAULT_INCREMENT = false; - - @Option(names = "--field", required = true, description = "Field containing the strings to add.", paramLabel = "") - private String stringField; - - @Option(names = "--score", description = "Name of the field to use for scores.", paramLabel = "") - private String scoreField; - - @Option(names = "--score-default", description = "Score when field not present (default: ${DEFAULT-VALUE}).", paramLabel = "") - private double defaultScore = DEFAULT_SCORE; - - @Option(names = "--payload", description = "Field containing the payload.", paramLabel = "") - private String payloadField; - - @Option(names = "--increment", description = "Increment the existing suggestion by the score instead of replacing the score.") - private boolean increment = DEFAULT_INCREMENT; - - public String getStringField() { - return stringField; - } - - public void setStringField(String field) { - this.stringField = field; - } - - public String getScoreField() { - return scoreField; - } - - public void setScore(String field) { - this.scoreField = field; - } - - public double getDefaultScore() { - return defaultScore; - } - - public void setDefaultScore(double scoreDefault) { - this.defaultScore = scoreDefault; - } - - public String getPayloadField() { - return payloadField; - } - - public void setPayload(String field) { - this.payloadField = field; - } - - public boolean isIncrement() { - return increment; - } - - public void setIncrement(boolean increment) { - this.increment = increment; - } - - @Override - protected SugaddBuilder operationBuilder() { - SugaddBuilder supplier = new SugaddBuilder(); - supplier.setDefaultScore(defaultScore); - supplier.setIncrement(increment); - supplier.setStringField(stringField); - supplier.setPayloadField(payloadField); - supplier.setScoreField(scoreField); - return supplier; - } - - } - - @Command(name = "ts.add", description = "Add samples to RedisTimeSeries") - public static class TsAddCommand extends OperationCommand { - - @Option(names = "--timestamp", description = "Name of the field to use for timestamps. If unset, uses auto-timestamping.", paramLabel = "") - private String timestampField; - - @Option(names = "--value", required = true, description = "Name of the field to use for values.", paramLabel = "") - private String valueField; - - @Option(names = "--on-duplicate", description = "Duplicate policy: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE}).", paramLabel = "") - private DuplicatePolicy duplicatePolicy = TsAddBuilder.DEFAULT_DUPLICATE_POLICY; - - @Option(arity = "1..*", names = "--labels", description = "Labels in the form label1=field1 label2=field2...", paramLabel = "SPEL") - private Map labels = new LinkedHashMap<>(); - - @Override - protected TsAddBuilder operationBuilder() { - TsAddBuilder supplier = new TsAddBuilder(); - supplier.setDuplicatePolicy(duplicatePolicy); - supplier.setTimestampField(timestampField); - supplier.setValueField(valueField); - supplier.setLabels(labels); - return supplier; - } - - } - - @Command(name = "xadd", description = "Append entries to a stream") - public static class XaddCommand extends OperationCommand { - - @Mixin - private FieldFilteringArgs filteringOptions = new FieldFilteringArgs(); - - @Option(names = "--maxlen", description = "Stream maxlen.", paramLabel = "") - private long maxlen; - - @Option(names = "--trim", description = "Stream efficient trimming ('~' flag).") - private boolean approximateTrimming; - - public FieldFilteringArgs getFilteringOptions() { - return filteringOptions; - } - - public void setFilteringOptions(FieldFilteringArgs filteringOptions) { - this.filteringOptions = filteringOptions; - } - - public long getMaxlen() { - return maxlen; - } - - public void setMaxlen(long maxlen) { - this.maxlen = maxlen; - } - - public boolean isApproximateTrimming() { - return approximateTrimming; - } - - public void setApproximateTrimming(boolean approximateTrimming) { - this.approximateTrimming = approximateTrimming; - } - - @Override - protected XaddSupplier operationBuilder() { - XaddSupplier supplier = new XaddSupplier(); - supplier.setApproximateTrimming(approximateTrimming); - supplier.setMaxlen(maxlen); - return supplier; - } - - } - - @Command(name = "zadd", description = "Add members with scores to a sorted set") - public static class ZaddCommand extends AbstractCollectionCommand { - - @Option(names = "--score", description = "Name of the field to use for scores.", paramLabel = "") - private String scoreField; - - @Option(names = "--score-default", description = "Score when field not present (default: ${DEFAULT-VALUE}).", paramLabel = "") - private double defaultScore = ZaddSupplier.DEFAULT_SCORE; - - @Override - protected ZaddSupplier collectionOperationBuilder() { - ZaddSupplier supplier = new ZaddSupplier(); - supplier.setScoreField(scoreField); - supplier.setDefaultScore(defaultScore); - return supplier; - } - - } - - protected abstract static class AbstractCollectionCommand extends OperationCommand { - - @Option(names = "--member-space", description = "Keyspace prefix for member IDs.", paramLabel = "") - private String memberSpace; - - @Option(arity = "1..*", names = { "-m", - "--members" }, description = "Member field names for collections.", paramLabel = "") - private List memberFields; - - @Override - protected AbstractMapOperationBuilder operationBuilder() { - AbstractCollectionMapOperationBuilder builder = collectionOperationBuilder(); - builder.setMemberSpace(memberSpace); - builder.setMemberFields(memberFields); - return builder; - } - - protected abstract AbstractCollectionMapOperationBuilder collectionOperationBuilder(); - - } - - @Command(mixinStandardHelpOptions = true) - protected abstract static class OperationCommand extends BaseCommand { - - @Option(names = { "-p", "--keyspace" }, description = "Keyspace prefix.", paramLabel = "") - private String keyspace; - - @Option(names = { "-k", "--keys" }, arity = "1..*", description = "Key fields.", paramLabel = "") - private List keys; - - @Option(names = { "-s", - "--separator" }, description = "Key separator (default: ${DEFAULT-VALUE}).", paramLabel = "") - private String keySeparator = AbstractMapOperationBuilder.DEFAULT_SEPARATOR; - - @Option(names = { "-r", "--remove" }, description = "Remove key or member fields the first time they are used.") - private boolean removeFields = AbstractMapOperationBuilder.DEFAULT_REMOVE_FIELDS; - - @Option(names = "--ignore-missing", description = "Ignore missing fields.") - private boolean ignoreMissingFields = AbstractMapOperationBuilder.DEFAULT_IGNORE_MISSING_FIELDS; - - public Operation, Object> operation() { - AbstractMapOperationBuilder builder = operationBuilder(); - builder.setIgnoreMissingFields(ignoreMissingFields); - builder.setKeyFields(keys); - builder.setKeySeparator(keySeparator); - builder.setKeyspace(keyspace); - builder.setRemoveFields(removeFields); - return builder.build(); - } - - protected abstract AbstractMapOperationBuilder operationBuilder(); - - } - } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractJobCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractJobCommand.java index 0a312ea35..06cd54dbf 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractJobCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractJobCommand.java @@ -18,7 +18,7 @@ import me.tongfei.progressbar.ProgressBarStyle; import picocli.CommandLine.Option; -abstract class AbstractJobCommand extends AbstractRiotCommand { +abstract class AbstractJobCommand extends AbstractSubCommand { public enum ProgressStyle { BLOCK, BAR, ASCII, LOG, NONE @@ -71,19 +71,19 @@ private ProgressBarStyle progressBarStyle() { } @Override - protected AbstractJobRunnable executable() { - AbstractJobRunnable executable = jobExecutable(); + protected AbstractJobRunnable runnable() { + AbstractJobRunnable runnable = jobRunnable(); if (name != null) { - executable.setName(name); + runnable.setName(name); } - executable.setChunkSize(chunkSize); - executable.setDryRun(dryRun); - executable.setRetryLimit(retryLimit); - executable.setSkipLimit(skipLimit); - executable.setSleep(Duration.ofMillis(sleep)); - executable.setThreads(threads); - executable.setStepConfigurer(this::configureStep); - return executable; + runnable.setChunkSize(chunkSize); + runnable.setDryRun(dryRun); + runnable.setRetryLimit(retryLimit); + runnable.setSkipLimit(skipLimit); + runnable.setSleep(Duration.ofMillis(sleep)); + runnable.setThreads(threads); + runnable.setStepConfigurer(this::configureStep); + return runnable; } private void configureStep(RiotStep step) { @@ -120,6 +120,6 @@ protected Callable initialMaxSupplier(RiotStep step) { return () -> ProgressStepExecutionListener.UNKNOWN_SIZE; } - protected abstract AbstractJobRunnable jobExecutable(); + protected abstract AbstractJobRunnable jobRunnable(); } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractMainCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractMainCommand.java new file mode 100644 index 000000000..559f80cbc --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractMainCommand.java @@ -0,0 +1,94 @@ +package com.redis.riot.cli; + +import java.io.PrintWriter; + +import org.springframework.expression.Expression; + +import com.redis.riot.core.RiotUtils; +import com.redis.riot.core.TemplateExpression; +import com.redis.spring.batch.common.Range; + +import picocli.AutoComplete.GenerateCompletion; +import picocli.CommandLine; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.IExecutionStrategy; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParseResult; +import picocli.CommandLine.RunFirst; +import picocli.CommandLine.RunLast; +import picocli.CommandLine.Spec; + +@Command(subcommands = { DatabaseImportCommand.class, DatabaseExportCommand.class, FileDumpImportCommand.class, + FileImportCommand.class, FileDumpExportCommand.class, FakerImportCommand.class, GenerateCommand.class, + ReplicateCommand.class, PingCommand.class, GenerateCompletion.class }) +public abstract class AbstractMainCommand extends BaseCommand implements Runnable { + + PrintWriter out; + + PrintWriter err; + + @Spec + CommandSpec spec; + + @ArgGroup(exclusive = false, heading = "Redis connection options%n") + RedisArgs redisArgs = new RedisArgs(); + + @Option(names = { "-V", "--version" }, versionHelp = true, description = "Print version information and exit.") + boolean versionRequested; + + @Override + public void run() { + spec.commandLine().usage(out); + } + + public static int run(AbstractMainCommand cmd, String... args) { + CommandLine commandLine = new CommandLine(cmd); + cmd.out = commandLine.getOut(); + cmd.err = commandLine.getErr(); + return execute(commandLine, args); + } + + public static int run(AbstractMainCommand cmd, PrintWriter out, PrintWriter err, String[] args, + IExecutionStrategy... executionStrategies) { + CommandLine commandLine = new CommandLine(cmd); + commandLine.setOut(out); + commandLine.setErr(err); + cmd.out = out; + cmd.err = err; + return execute(commandLine, args, executionStrategies); + } + + private static int execute(CommandLine commandLine, String[] args, IExecutionStrategy... executionStrategies) { + CompositeExecutionStrategy executionStrategy = new CompositeExecutionStrategy(); + executionStrategy.addDelegates(executionStrategies); + executionStrategy.addDelegates(LoggingMixin::executionStrategy); + executionStrategy.addDelegates(AbstractMainCommand::executionStrategy); + commandLine.setExecutionStrategy(executionStrategy); + commandLine.registerConverter(Range.class, Range::of); + commandLine.registerConverter(Expression.class, RiotUtils::parse); + commandLine.registerConverter(TemplateExpression.class, RiotUtils::parseTemplate); + commandLine.setCaseInsensitiveEnumValuesAllowed(true); + commandLine.setUnmatchedOptionsAllowedAsOptionParameters(false); + return commandLine.execute(args); + } + + private static int executionStrategy(ParseResult parseResult) { + for (ParseResult subcommand : parseResult.subcommands()) { + Object command = subcommand.commandSpec().userObject(); + if (AbstractImportCommand.class.isAssignableFrom(command.getClass())) { + AbstractImportCommand importCommand = (AbstractImportCommand) command; + for (ParseResult redisCommand : subcommand.subcommands()) { + if (redisCommand.isUsageHelpRequested()) { + return new RunLast().execute(redisCommand); + } + importCommand.getCommands().add((RedisCommand) redisCommand.commandSpec().userObject()); + } + return new RunFirst().execute(subcommand); + } + } + return new RunLast().execute(parseResult); // default execution strategy + } + +} diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractManifestVersionProvider.java b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractManifestVersionProvider.java new file mode 100644 index 000000000..0f47f5020 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractManifestVersionProvider.java @@ -0,0 +1,63 @@ +package com.redis.riot.cli; + +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.Optional; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import picocli.CommandLine.IVersionProvider; + +/** + * {@link picocli.CommandLine.IVersionProvider} implementation that returns + * version information from the jar file's {@code /META-INF/MANIFEST.MF} file. + */ +public abstract class AbstractManifestVersionProvider implements IVersionProvider { + + public String getVersionString() { + try { + Enumeration resources = AbstractManifestVersionProvider.class.getClassLoader() + .getResources("META-INF/MANIFEST.MF"); + while (resources.hasMoreElements()) { + Optional version = version(resources.nextElement()); + if (version.isPresent()) { + return version.get(); + } + } + } catch (IOException e) { + // Fail silently + } + return "N/A"; + } + + private Optional version(URL url) { + try { + Manifest manifest = new Manifest(url.openStream()); + if (isApplicableManifest(manifest)) { + Attributes attr = manifest.getMainAttributes(); + return Optional.of(String.valueOf(get(attr, "Implementation-Version"))); + } + return Optional.empty(); + } catch (IOException e) { + Logger log = LoggerFactory.getLogger(AbstractManifestVersionProvider.class); + log.warn("Unable to read from {}", url, e); + return Optional.of("N/A"); + } + } + + private boolean isApplicableManifest(Manifest manifest) { + Attributes attributes = manifest.getMainAttributes(); + return getImplementationTitle().equals(get(attributes, "Implementation-Title")); + } + + protected abstract String getImplementationTitle(); + + private static Object get(Attributes attributes, String key) { + return attributes.get(new Attributes.Name(key)); + } + +} diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractRiotCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractRiotCommand.java deleted file mode 100644 index ca4611ce9..000000000 --- a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractRiotCommand.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.redis.riot.cli; - -import java.util.Map; - -import org.springframework.expression.Expression; - -import com.redis.riot.core.AbstractRiotRunnable; - -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; -import picocli.CommandLine.ParentCommand; - -@Command -public abstract class AbstractRiotCommand extends BaseCommand implements Runnable { - - @ParentCommand - protected Main parent; - - @Option(arity = "1..*", names = "--var", description = "SpEL expressions for context variables, in the form var=\"exp\"", paramLabel = "") - Map variableExpressions; - - @Option(names = "--date-format", description = "Date/time format (default: ${DEFAULT-VALUE}). For details see https://www.baeldung.com/java-simple-date-format#date_time_patterns", paramLabel = "") - String dateFormat = AbstractRiotRunnable.DEFAULT_DATE_FORMAT; - - @Override - public void run() { - AbstractRiotRunnable executable = executable(); - executable.setRedisOptions(parent.redisArgs.redisOptions()); - executable.setVarExpressions(variableExpressions); - executable.setDateFormat(dateFormat); - executable.run(); - } - - protected abstract AbstractRiotRunnable executable(); - -} diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractStructImportCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractStructImportCommand.java index daf822b90..95744c896 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractStructImportCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractStructImportCommand.java @@ -6,16 +6,16 @@ public abstract class AbstractStructImportCommand extends AbstractJobCommand { - @ArgGroup(exclusive = false, heading = "Writer options%n") - RedisWriterArgs writerArgs = new RedisWriterArgs(); + @ArgGroup(exclusive = false, heading = "Writer options%n") + RedisWriterArgs writerArgs = new RedisWriterArgs(); - @Override - protected AbstractStructImport jobExecutable() { - AbstractStructImport executable = importExecutable(); - executable.setWriterOptions(writerArgs.writerOptions()); - return executable; - } + @Override + protected AbstractStructImport jobRunnable() { + AbstractStructImport runnable = importRunnable(); + runnable.setWriterOptions(writerArgs.writerOptions()); + return runnable; + } - protected abstract AbstractStructImport importExecutable(); + protected abstract AbstractStructImport importRunnable(); } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/AbstractSubCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractSubCommand.java new file mode 100644 index 000000000..98b786390 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/AbstractSubCommand.java @@ -0,0 +1,23 @@ +package com.redis.riot.cli; + +import com.redis.riot.core.AbstractRunnable; + +import picocli.CommandLine.Command; +import picocli.CommandLine.ParentCommand; + +@Command +public abstract class AbstractSubCommand extends BaseCommand implements Runnable { + + @ParentCommand + protected AbstractMainCommand parent; + + @Override + public void run() { + AbstractRunnable runnable = runnable(); + runnable.setRedisClientOptions(parent.redisArgs.redisOptions()); + runnable.run(); + } + + protected abstract AbstractRunnable runnable(); + +} diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/BaseCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/BaseCommand.java index da7d8eaac..4460ca3e8 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/BaseCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/BaseCommand.java @@ -1,43 +1,19 @@ package com.redis.riot.cli; -import java.util.ResourceBundle; - -import org.slf4j.helpers.MessageFormatter; - import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; -import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Option; -import picocli.CommandLine.Spec; /** * @author Julien Ruaux */ -@Command(versionProvider = ManifestVersionProvider.class, resourceBundle = "com.redis.riot.Messages", usageHelpAutoWidth = true, abbreviateSynopsis = true) +@Command(usageHelpAutoWidth = true, abbreviateSynopsis = true) public class BaseCommand { - static { - if (System.getenv().containsKey("RIOT_NO_COLOR")) { - System.setProperty("picocli.ansi", "false"); - } - } - - @Spec - CommandSpec spec; - - ResourceBundle bundle = ResourceBundle.getBundle("com.redis.riot.Messages"); - - @Option(names = { "-H", "--help" }, usageHelp = true, description = "Show this help message and exit.") - boolean helpRequested; - - @Mixin - LoggingMixin loggingMixin = new LoggingMixin(); + @Option(names = "--help", usageHelp = true, description = "Show this help message and exit.") + boolean helpRequested; - protected String getString(String key, Object... args) { - if (null == args || args.length == 0) { - return bundle.getString(key); - } - return MessageFormatter.arrayFormat(bundle.getString(key), args).getMessage(); - } + @Mixin + LoggingMixin loggingMixin = new LoggingMixin(); } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/DatabaseExportCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/DatabaseExportCommand.java index d2699ce60..e4c6a72d7 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/DatabaseExportCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/DatabaseExportCommand.java @@ -16,13 +16,13 @@ public class DatabaseExportCommand extends AbstractExportCommand { DatabaseExportArgs args = new DatabaseExportArgs(); @Override - protected DatabaseExport exportExecutable() { - DatabaseExport executable = new DatabaseExport(); - executable.setSql(sql); - executable.setAssertUpdates(args.assertUpdates); - executable.setDataSourceOptions(args.dataSourceOptions()); - executable.setKeyRegex(args.keyRegex); - return executable; + protected DatabaseExport exportRunnable() { + DatabaseExport runnable = new DatabaseExport(); + runnable.setSql(sql); + runnable.setAssertUpdates(args.assertUpdates); + runnable.setDataSourceOptions(args.dataSourceOptions()); + runnable.setKeyRegex(args.keyRegex); + return runnable; } } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/DatabaseImportCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/DatabaseImportCommand.java index 291fa0e20..7a494b78a 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/DatabaseImportCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/DatabaseImportCommand.java @@ -10,21 +10,21 @@ @Command(name = "db-import", description = "Import from a relational database.") public class DatabaseImportCommand extends AbstractImportCommand { - @Parameters(arity = "1", description = "SQL SELECT statement", paramLabel = "SQL") - String sql; + @Parameters(arity = "1", description = "SQL SELECT statement", paramLabel = "SQL") + String sql; - @ArgGroup(exclusive = false) - DatabaseImportArgs args = new DatabaseImportArgs(); + @ArgGroup(exclusive = false) + DatabaseImportArgs args = new DatabaseImportArgs(); - @Override - protected AbstractImport importExecutable() { - DatabaseImport executable = new DatabaseImport(); - executable.setSql(sql); - executable.setDataSourceOptions(args.dataSourceOptions()); - executable.setFetchSize(args.fetchSize); - executable.setMaxItemCount(args.maxItemCount); - executable.setMaxResultSetRows(args.maxResultSetRows); - return executable; - } + @Override + protected AbstractImport importRunnable() { + DatabaseImport runnable = new DatabaseImport(); + runnable.setSql(sql); + runnable.setDataSourceOptions(args.dataSourceOptions()); + runnable.setFetchSize(args.fetchSize); + runnable.setMaxItemCount(args.maxItemCount); + runnable.setMaxResultSetRows(args.maxResultSetRows); + return runnable; + } } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/EvaluationContextArgs.java b/plugins/riot/src/main/java/com/redis/riot/cli/EvaluationContextArgs.java new file mode 100644 index 000000000..e23edff99 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/EvaluationContextArgs.java @@ -0,0 +1,26 @@ +package com.redis.riot.cli; + +import java.util.Map; + +import org.springframework.expression.Expression; + +import com.redis.riot.core.EvaluationContextOptions; + +import picocli.CommandLine.Option; + +public class EvaluationContextArgs { + + @Option(arity = "1..*", names = "--var", description = "SpEL expressions for context variables, in the form var=\"exp\"", paramLabel = "") + Map variableExpressions; + + @Option(names = "--date-format", description = "Date/time format (default: ${DEFAULT-VALUE}). For details see https://www.baeldung.com/java-simple-date-format#date_time_patterns", paramLabel = "") + String dateFormat = EvaluationContextOptions.DEFAULT_DATE_FORMAT; + + EvaluationContextOptions evaluationContextOptions() { + EvaluationContextOptions options = new EvaluationContextOptions(); + options.setVarExpressions(variableExpressions); + options.setDateFormat(dateFormat); + return options; + } + +} diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/FakerImportCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/FakerImportCommand.java index 2f804fb9f..0e3b910c7 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/FakerImportCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/FakerImportCommand.java @@ -29,13 +29,13 @@ public class FakerImportCommand extends AbstractImportCommand { Locale locale = FakerImport.DEFAULT_LOCALE; @Override - protected FakerImport importExecutable() { - FakerImport executable = new FakerImport(); - executable.setFields(fields); - executable.setCount(count); - executable.setLocale(locale); - executable.setSearchIndex(searchIndex); - return executable; + protected FakerImport importRunnable() { + FakerImport runnable = new FakerImport(); + runnable.setFields(fields); + runnable.setCount(count); + runnable.setLocale(locale); + runnable.setSearchIndex(searchIndex); + return runnable; } @Override diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/FileDumpExportCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/FileDumpExportCommand.java index 71c52cb8b..4705f92f0 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/FileDumpExportCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/FileDumpExportCommand.java @@ -8,20 +8,20 @@ @Command(name = "file-export", description = "Export Redis data to JSON or XML files.") public class FileDumpExportCommand extends AbstractExportCommand { - @ArgGroup(exclusive = false) - FileDumpExportArgs args = new FileDumpExportArgs(); + @ArgGroup(exclusive = false) + FileDumpExportArgs args = new FileDumpExportArgs(); - @Override - protected FileDumpExport exportExecutable() { - FileDumpExport executable = new FileDumpExport(); - executable.setFile(args.file); - executable.setAppend(args.append); - executable.setElementName(args.elementName); - executable.setLineSeparator(args.lineSeparator); - executable.setRootName(args.rootName); - executable.setFileOptions(args.fileOptions()); - executable.setType(args.type); - return executable; - } + @Override + protected FileDumpExport exportRunnable() { + FileDumpExport runnable = new FileDumpExport(); + runnable.setFile(args.file); + runnable.setAppend(args.append); + runnable.setElementName(args.elementName); + runnable.setLineSeparator(args.lineSeparator); + runnable.setRootName(args.rootName); + runnable.setFileOptions(args.fileOptions()); + runnable.setType(args.type); + return runnable; + } } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/FileDumpImportCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/FileDumpImportCommand.java index 8bc36ef91..db769282f 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/FileDumpImportCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/FileDumpImportCommand.java @@ -13,12 +13,12 @@ public class FileDumpImportCommand extends AbstractStructImportCommand { FileDumpImportArgs args = new FileDumpImportArgs(); @Override - protected FileDumpImport importExecutable() { - FileDumpImport executable = new FileDumpImport(); - executable.setFiles(args.files); - executable.setFileOptions(args.fileOptions()); - executable.setType(args.type); - return executable; + protected FileDumpImport importRunnable() { + FileDumpImport runnable = new FileDumpImport(); + runnable.setFiles(args.files); + runnable.setFileOptions(args.fileOptions()); + runnable.setType(args.type); + return runnable; } @Override diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/FileImportCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/FileImportCommand.java index aea94c72f..1306df6d5 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/FileImportCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/FileImportCommand.java @@ -13,23 +13,23 @@ public class FileImportCommand extends AbstractImportCommand { FileImportArgs args = new FileImportArgs(); @Override - protected AbstractImport importExecutable() { - FileImport executable = new FileImport(); - executable.setFiles(args.files); - executable.setColumnRanges(args.columnRanges); - executable.setContinuationString(args.continuationString); - executable.setDelimiter(args.delimiter); - executable.setFields(args.fields); - executable.setFileOptions(args.fileOptions()); - executable.setFileType(args.fileType); - executable.setHeader(args.header); - executable.setHeaderLine(args.headerLine); - executable.setIncludedFields(args.includedFields); - executable.setLinesToSkip(args.linesToSkip); - executable.setMaxItemCount(args.maxItemCount); - executable.setQuoteCharacter(args.quoteCharacter); - executable.setRegexes(args.regexes); - return executable; + protected AbstractImport importRunnable() { + FileImport runnable = new FileImport(); + runnable.setFiles(args.files); + runnable.setColumnRanges(args.columnRanges); + runnable.setContinuationString(args.continuationString); + runnable.setDelimiter(args.delimiter); + runnable.setFields(args.fields); + runnable.setFileOptions(args.fileOptions()); + runnable.setFileType(args.fileType); + runnable.setHeader(args.header); + runnable.setHeaderLine(args.headerLine); + runnable.setIncludedFields(args.includedFields); + runnable.setLinesToSkip(args.linesToSkip); + runnable.setMaxItemCount(args.maxItemCount); + runnable.setQuoteCharacter(args.quoteCharacter); + runnable.setRegexes(args.regexes); + return runnable; } } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/GenerateCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/GenerateCommand.java index c354440e0..969a09e32 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/GenerateCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/GenerateCommand.java @@ -5,8 +5,8 @@ import java.util.List; import java.util.concurrent.Callable; -import com.redis.riot.core.GeneratorImport; import com.redis.riot.core.RiotStep; +import com.redis.riot.redis.GeneratorImport; import com.redis.spring.batch.common.DataType; import com.redis.spring.batch.common.Range; import com.redis.spring.batch.gen.CollectionOptions; @@ -102,22 +102,22 @@ protected Callable initialMaxSupplier(RiotStep step) { } @Override - protected GeneratorImport importExecutable() { - GeneratorImport executable = new GeneratorImport(); - executable.setCount(count); - executable.setExpiration(expiration); - executable.setHashOptions(hashOptions()); - executable.setJsonOptions(jsonOptions()); - executable.setKeyRange(keyRange); - executable.setKeyspace(keyspace); - executable.setListOptions(listOptions()); - executable.setSetOptions(setOptions()); - executable.setStreamOptions(streamOptions()); - executable.setStringOptions(stringOptions()); - executable.setTimeSeriesOptions(timeseriesOptions()); - executable.setTypes(types); - executable.setZsetOptions(zsetOptions()); - return executable; + protected GeneratorImport importRunnable() { + GeneratorImport runnable = new GeneratorImport(); + runnable.setCount(count); + runnable.setExpiration(expiration); + runnable.setHashOptions(hashOptions()); + runnable.setJsonOptions(jsonOptions()); + runnable.setKeyRange(keyRange); + runnable.setKeyspace(keyspace); + runnable.setListOptions(listOptions()); + runnable.setSetOptions(setOptions()); + runnable.setStreamOptions(streamOptions()); + runnable.setStringOptions(stringOptions()); + runnable.setTimeSeriesOptions(timeseriesOptions()); + runnable.setTypes(types); + runnable.setZsetOptions(zsetOptions()); + return runnable; } private ZsetOptions zsetOptions() { diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/KeyValueProcessorArgs.java b/plugins/riot/src/main/java/com/redis/riot/cli/KeyValueProcessorArgs.java index 8a9ef0193..cf742e9e0 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/KeyValueProcessorArgs.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/KeyValueProcessorArgs.java @@ -2,7 +2,7 @@ import org.springframework.expression.Expression; -import com.redis.riot.core.KeyValueProcessorOptions; +import com.redis.riot.core.ExportProcessorOptions; import com.redis.riot.core.TemplateExpression; import picocli.CommandLine.Option; @@ -24,8 +24,8 @@ public class KeyValueProcessorArgs { @Option(names = "--no-stream-id", description = "Drop IDs from source stream messages instead of passing them along to the target.") boolean dropStreamMessageIds; - public KeyValueProcessorOptions processorOptions() { - KeyValueProcessorOptions options = new KeyValueProcessorOptions(); + public ExportProcessorOptions processorOptions() { + ExportProcessorOptions options = new ExportProcessorOptions(); options.setKeyExpression(keyExpression); options.setTtlExpression(ttlExpression); options.setTypeExpression(typeExpression); diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/LoggingMixin.java b/plugins/riot/src/main/java/com/redis/riot/cli/LoggingMixin.java index b47d7e64b..f5e758ae1 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/LoggingMixin.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/LoggingMixin.java @@ -7,6 +7,7 @@ import java.util.Map.Entry; import org.slf4j.event.Level; +import org.slf4j.simple.SimpleLogger; import picocli.CommandLine.ExecutionException; import picocli.CommandLine.ExitCode; @@ -33,7 +34,7 @@ public class LoggingMixin { boolean levelInBrackets; Map logLevels = new LinkedHashMap<>(); - @Option(names = { "-d", "--debug" }, description = "Log in debug mode (includes normal stacktrace).") + @Option(names = { "-d", "--debug" }, description = "Log in debug mode.") public void setDebug(boolean debug) { if (debug) { getTopLevelCommandLoggingMixin(mixee).level = Level.DEBUG; @@ -61,12 +62,12 @@ public void setError(boolean error) { } } - @Option(arity = "1..*", names = "--log-levels", description = "Custom log levels (e.g.: com.amazonaws=ERROR io.lettuce=INFO io.netty=INFO).", paramLabel = "") + @Option(arity = "1..*", names = "--log", description = "Custom log levels (e.g.: io.lettuce=INFO).", paramLabel = "") public void setLogLevels(Map levels) { getTopLevelCommandLoggingMixin(mixee).logLevels = levels; } - @Option(names = "--log-file", description = "Log output target which can be a file path or special values System.out and System.err (default: System.err).", paramLabel = "") + @Option(names = "--log-file", description = "Log output target. Can be a path or special values System.out and System.err (default: System.err).", paramLabel = "") public void setLogFile(String file) { getTopLevelCommandLoggingMixin(mixee).logFile = file; } @@ -101,7 +102,7 @@ public void setShowShortLogName(boolean show) { getTopLevelCommandLoggingMixin(mixee).showShortLogName = show; } - @Option(names = "--log-level-brackets", description = "Output log level string in brackets.", hidden = true) + @Option(names = "--log-level", description = "Output log level string in brackets.", hidden = true) public void setLevelInBrackets(boolean enable) { getTopLevelCommandLoggingMixin(mixee).levelInBrackets = enable; } @@ -112,7 +113,7 @@ public static int executionStrategy(ParseResult parseResult) throws ExecutionExc } private static LoggingMixin getTopLevelCommandLoggingMixin(CommandSpec commandSpec) { - return ((Main) commandSpec.root().userObject()).loggingMixin; + return ((AbstractMainCommand) commandSpec.root().userObject()).loggingMixin; } public void configureLogging() { @@ -120,24 +121,30 @@ public void configureLogging() { } private static void configureLogging(LoggingMixin mixin) { - setProperty(org.slf4j.simple.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, mixin.level.name()); + setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, mixin.level.name()); if (mixin.logFile != null) { - setProperty(org.slf4j.simple.SimpleLogger.LOG_FILE_KEY, mixin.logFile); + setProperty(SimpleLogger.LOG_FILE_KEY, mixin.logFile); } - setBoolean(org.slf4j.simple.SimpleLogger.SHOW_DATE_TIME_KEY, mixin.showDateTime); + setBoolean(SimpleLogger.SHOW_DATE_TIME_KEY, mixin.showDateTime); if (mixin.dateTimeFormat != null) { - setProperty(org.slf4j.simple.SimpleLogger.DATE_TIME_FORMAT_KEY, mixin.dateTimeFormat); + setProperty(SimpleLogger.DATE_TIME_FORMAT_KEY, mixin.dateTimeFormat); } - setBoolean(org.slf4j.simple.SimpleLogger.SHOW_THREAD_ID_KEY, mixin.showThreadId); - setBoolean(org.slf4j.simple.SimpleLogger.SHOW_THREAD_NAME_KEY, mixin.showThreadName); - setBoolean(org.slf4j.simple.SimpleLogger.SHOW_LOG_NAME_KEY, mixin.showLogName); - setBoolean(org.slf4j.simple.SimpleLogger.SHOW_SHORT_LOG_NAME_KEY, mixin.showShortLogName); - setBoolean(org.slf4j.simple.SimpleLogger.LEVEL_IN_BRACKETS_KEY, mixin.levelInBrackets); + setBoolean(SimpleLogger.SHOW_THREAD_ID_KEY, mixin.showThreadId); + setBoolean(SimpleLogger.SHOW_THREAD_NAME_KEY, mixin.showThreadName); + setBoolean(SimpleLogger.SHOW_LOG_NAME_KEY, mixin.showLogName); + setBoolean(SimpleLogger.SHOW_SHORT_LOG_NAME_KEY, mixin.showShortLogName); + setBoolean(SimpleLogger.LEVEL_IN_BRACKETS_KEY, mixin.levelInBrackets); + setLogLevel("org.springframework.batch.core.step.builder.FaultTolerantStepBuilder", Level.ERROR); + setLogLevel("org.springframework.batch.core.step.item.ChunkMonitor", Level.ERROR); for (Entry entry : mixin.logLevels.entrySet()) { - System.setProperty(org.slf4j.simple.SimpleLogger.LOG_KEY_PREFIX + entry.getKey(), entry.getValue().name()); + setLogLevel(entry.getKey(), entry.getValue()); } } + private static void setLogLevel(String key, Level level) { + System.setProperty(SimpleLogger.LOG_KEY_PREFIX + key, level.name()); + } + private static void setBoolean(String property, boolean value) { if (value) { setProperty(property, String.valueOf(value)); diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/Main.java b/plugins/riot/src/main/java/com/redis/riot/cli/Main.java index 0815d4e24..fe55e6b5a 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/Main.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/Main.java @@ -1,95 +1,12 @@ package com.redis.riot.cli; -import java.io.PrintWriter; - -import org.springframework.expression.Expression; - -import com.redis.riot.cli.AbstractImportCommand.OperationCommand; -import com.redis.riot.core.RiotUtils; -import com.redis.riot.core.TemplateExpression; -import com.redis.spring.batch.common.Range; - -import picocli.AutoComplete.GenerateCompletion; -import picocli.CommandLine; -import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; -import picocli.CommandLine.IExecutionStrategy; -import picocli.CommandLine.Option; -import picocli.CommandLine.ParseResult; -import picocli.CommandLine.RunFirst; -import picocli.CommandLine.RunLast; - -@Command(name = "riot", subcommands = { DatabaseImportCommand.class, DatabaseExportCommand.class, - FileDumpImportCommand.class, FileImportCommand.class, FileDumpExportCommand.class, FakerImportCommand.class, - GenerateCommand.class, ReplicateCommand.class, PingCommand.class, GenerateCompletion.class }) -public class Main extends BaseCommand implements Runnable { - - PrintWriter out; - PrintWriter err; - - @Option(names = { "-V", "--version" }, versionHelp = true, description = "Print version information and exit.") - boolean versionRequested; - - @ArgGroup(exclusive = false, heading = "Redis connection options%n") - RedisArgs redisArgs = new RedisArgs(); - - @Override - public void run() { - spec.commandLine().usage(out); - } +@Command(name = "riot", versionProvider = ManifestVersionProvider.class, headerHeading = "RIOT is a data import/export tool for Redis.%n%n", footerHeading = "%nDocumentation found at https://developer.redis.com/riot%n") +public class Main extends AbstractMainCommand { public static void main(String[] args) { - System.exit(run(args)); - } - - public static int run(String... args) { - Main cmd = new Main(); - CommandLine commandLine = new CommandLine(cmd); - cmd.out = commandLine.getOut(); - cmd.err = commandLine.getErr(); - return execute(commandLine, args); - } - - public static int run(PrintWriter out, PrintWriter err, String[] args, IExecutionStrategy... executionStrategies) { - Main cmd = new Main(); - CommandLine commandLine = new CommandLine(cmd); - commandLine.setOut(out); - commandLine.setErr(err); - cmd.out = out; - cmd.err = err; - return execute(commandLine, args, executionStrategies); - } - - private static int execute(CommandLine commandLine, String[] args, IExecutionStrategy... executionStrategies) { - CompositeExecutionStrategy executionStrategy = new CompositeExecutionStrategy(); - executionStrategy.addDelegates(executionStrategies); - executionStrategy.addDelegates(LoggingMixin::executionStrategy); - executionStrategy.addDelegates(Main::executionStrategy); - commandLine.setExecutionStrategy(executionStrategy); - commandLine.registerConverter(Range.class, Range::of); - commandLine.registerConverter(Expression.class, RiotUtils::parse); - commandLine.registerConverter(TemplateExpression.class, RiotUtils::parseTemplate); - commandLine.setCaseInsensitiveEnumValuesAllowed(true); - commandLine.setUnmatchedOptionsAllowedAsOptionParameters(false); - return commandLine.execute(args); - } - - private static int executionStrategy(ParseResult parseResult) { - for (ParseResult subcommand : parseResult.subcommands()) { - Object command = subcommand.commandSpec().userObject(); - if (AbstractImportCommand.class.isAssignableFrom(command.getClass())) { - AbstractImportCommand importCommand = (AbstractImportCommand) command; - for (ParseResult redisCommand : subcommand.subcommands()) { - if (redisCommand.isUsageHelpRequested()) { - return new RunLast().execute(redisCommand); - } - importCommand.getCommands().add((OperationCommand) redisCommand.commandSpec().userObject()); - } - return new RunFirst().execute(subcommand); - } - } - return new RunLast().execute(parseResult); // default execution strategy + System.exit(run(new Main(), args)); } } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/ManifestVersionProvider.java b/plugins/riot/src/main/java/com/redis/riot/cli/ManifestVersionProvider.java index c62a07642..b9e13d7a8 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/ManifestVersionProvider.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/ManifestVersionProvider.java @@ -1,72 +1,27 @@ package com.redis.riot.cli; -import java.io.IOException; -import java.net.URL; -import java.util.Enumeration; -import java.util.Optional; -import java.util.jar.Attributes; -import java.util.jar.Manifest; +public class ManifestVersionProvider extends AbstractManifestVersionProvider { -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + private static final String IMPLEMENTATION_TITLE = "riot"; -import picocli.CommandLine.IVersionProvider; + @Override + protected String getImplementationTitle() { + return IMPLEMENTATION_TITLE; + } -/** - * {@link picocli.CommandLine.IVersionProvider} implementation that returns version information from the jar file's - * {@code /META-INF/MANIFEST.MF} file. - */ -public class ManifestVersionProvider implements IVersionProvider { - - @Override - public String[] getVersion() throws Exception { - return new String[] { - // @formatter:off - "", " ▀ █ @|fg(4;1;1) ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄|@", + @Override + public String[] getVersion() throws Exception { + return new String[] { + // @formatter:off + "", + " ▀ █ @|fg(4;1;1) ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄|@", " █ ██ █ ███ ████ @|fg(4;2;1) ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄|@", " ██ █ █ █ █ @|fg(5;4;1) ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄|@", " █ █ █ █ █ @|fg(1;4;1) ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄|@", - " █ █ ███ ██ @|fg(0;3;4) ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄|@" + " v" + getVersionString(), "" }; - // @formatter:on - } - - public String getVersionString() { - try { - Enumeration resources = ManifestVersionProvider.class.getClassLoader().getResources("META-INF/MANIFEST.MF"); - while (resources.hasMoreElements()) { - Optional version = version(resources.nextElement()); - if (version.isPresent()) { - return version.get(); - } - } - } catch (IOException e) { - // Fail silently - } - return "N/A"; - } - - private Optional version(URL url) { - try { - Manifest manifest = new Manifest(url.openStream()); - if (isApplicableManifest(manifest)) { - Attributes attr = manifest.getMainAttributes(); - return Optional.of(String.valueOf(get(attr, "Implementation-Version"))); - } - return Optional.empty(); - } catch (IOException e) { - Logger log = LoggerFactory.getLogger(ManifestVersionProvider.class); - log.warn("Unable to read from {}", url, e); - return Optional.of("N/A"); - } - } - - private static boolean isApplicableManifest(Manifest manifest) { - Attributes attributes = manifest.getMainAttributes(); - return "riot".equals(get(attributes, "Implementation-Title")); - } - - private static Object get(Attributes attributes, String key) { - return attributes.get(new Attributes.Name(key)); - } + " █ █ ███ ██ @|fg(0;3;4) ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄|@" + " v" + getVersionString(), + "" + // @formatter:on + }; + } } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/PingCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/PingCommand.java index 5cce44c0c..56dd56633 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/PingCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/PingCommand.java @@ -2,39 +2,39 @@ import java.util.concurrent.TimeUnit; -import com.redis.riot.core.Ping; +import com.redis.riot.redis.Ping; import picocli.CommandLine.Command; import picocli.CommandLine.Option; @Command(name = "ping", description = "Test connectivity to a Redis server.") -public class PingCommand extends AbstractRiotCommand { - - @Option(names = "--iterations", description = "Number of test iterations. Use a negative value to test endlessly. (default: ${DEFAULT-VALUE}).", paramLabel = "") - int iterations = com.redis.riot.core.Ping.DEFAULT_ITERATIONS; - - @Option(names = "--count", description = "Number of pings to perform per iteration (default: ${DEFAULT-VALUE}).", paramLabel = "") - int count = com.redis.riot.core.Ping.DEFAULT_COUNT; - - @Option(names = "--unit", description = "Time unit used to display latencies (default: ${DEFAULT-VALUE}).", paramLabel = "") - TimeUnit timeUnit = com.redis.riot.core.Ping.DEFAULT_TIME_UNIT; - - @Option(names = "--distribution", description = "Show latency distribution.") - boolean latencyDistribution; - - @Option(arity = "0..*", names = "--percentiles", description = "Latency percentiles to display (default: ${DEFAULT-VALUE}).", paramLabel = "

") - double[] percentiles = Ping.defaultPercentiles(); - - @Override - protected Ping executable() { - Ping executable = new Ping(); - executable.setOut(parent.out); - executable.setCount(count); - executable.setIterations(iterations); - executable.setLatencyDistribution(latencyDistribution); - executable.setTimeUnit(timeUnit); - executable.setPercentiles(percentiles); - return executable; - } +public class PingCommand extends AbstractSubCommand { + + @Option(names = "--iterations", description = "Number of test iterations. Use a negative value to test endlessly. (default: ${DEFAULT-VALUE}).", paramLabel = "") + int iterations = Ping.DEFAULT_ITERATIONS; + + @Option(names = "--count", description = "Number of pings to perform per iteration (default: ${DEFAULT-VALUE}).", paramLabel = "") + int count = Ping.DEFAULT_COUNT; + + @Option(names = "--unit", description = "Time unit used to display latencies (default: ${DEFAULT-VALUE}).", paramLabel = "") + TimeUnit timeUnit = Ping.DEFAULT_TIME_UNIT; + + @Option(names = "--distribution", description = "Show latency distribution.") + boolean latencyDistribution; + + @Option(arity = "0..*", names = "--percentiles", description = "Latency percentiles to display (default: ${DEFAULT-VALUE}).", paramLabel = "

") + double[] percentiles = Ping.defaultPercentiles(); + + @Override + protected Ping runnable() { + Ping runnable = new Ping(); + runnable.setOut(parent.out); + runnable.setCount(count); + runnable.setIterations(iterations); + runnable.setLatencyDistribution(latencyDistribution); + runnable.setTimeUnit(timeUnit); + runnable.setPercentiles(percentiles); + return runnable; + } } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/RedisArgs.java b/plugins/riot/src/main/java/com/redis/riot/cli/RedisArgs.java index e55d91673..20a932261 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/RedisArgs.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/RedisArgs.java @@ -3,7 +3,7 @@ import java.io.File; import java.time.Duration; -import com.redis.riot.core.RedisOptions; +import com.redis.riot.core.RedisClientOptions; import io.lettuce.core.SslVerifyMode; import io.lettuce.core.protocol.ProtocolVersion; @@ -15,10 +15,10 @@ public class RedisArgs { String uri; @Option(names = { "-h", "--host" }, description = "Server hostname (default: ${DEFAULT-VALUE}).", paramLabel = "") - String host = RedisOptions.DEFAULT_HOST; + String host = RedisClientOptions.DEFAULT_HOST; @Option(names = { "-p", "--port" }, description = "Server port (default: ${DEFAULT-VALUE}).", paramLabel = "") - int port = RedisOptions.DEFAULT_PORT; + int port = RedisClientOptions.DEFAULT_PORT; @Option(names = { "-s", "--socket" }, description = "Server socket (overrides hostname and port).", paramLabel = "") String socket; @@ -81,8 +81,8 @@ public class RedisArgs { @Option(names = "--cacert", description = "X.509 CA certificate file to verify with.", paramLabel = "") File trustedCerts; - public RedisOptions redisOptions() { - RedisOptions options = new RedisOptions(); + public RedisClientOptions redisOptions() { + RedisClientOptions options = new RedisClientOptions(); options.setAutoReconnect(!noAutoReconnect); options.setCluster(cluster); options.setKey(key); diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/RedisCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/RedisCommand.java new file mode 100644 index 000000000..43c143b11 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/RedisCommand.java @@ -0,0 +1,11 @@ +package com.redis.riot.cli; + +import java.util.Map; + +import com.redis.spring.batch.common.Operation; + +public interface RedisCommand { + + Operation, Object> operation(); + +} diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/RedisReaderArgs.java b/plugins/riot/src/main/java/com/redis/riot/cli/RedisReaderArgs.java index a4ed20db9..ae82f6a54 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/RedisReaderArgs.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/RedisReaderArgs.java @@ -55,7 +55,7 @@ public class RedisReaderArgs { @ArgGroup(exclusive = false) KeyFilterArgs keyFilterArgs = new KeyFilterArgs(); - public void setIdleTimeout(Long timeout) { + public void setIdleTimeout(long timeout) { this.idleTimeout = timeout; } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/ReplicateCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/ReplicateCommand.java index 2e5bbcf65..dc550dc7a 100644 --- a/plugins/riot/src/main/java/com/redis/riot/cli/ReplicateCommand.java +++ b/plugins/riot/src/main/java/com/redis/riot/cli/ReplicateCommand.java @@ -7,12 +7,12 @@ import com.redis.riot.cli.RedisReaderArgs.ReadFromEnum; import com.redis.riot.core.AbstractExport; -import com.redis.riot.core.CompareMode; -import com.redis.riot.core.KeyComparisonStatusCountItemWriter; -import com.redis.riot.core.Replication; -import com.redis.riot.core.ReplicationMode; -import com.redis.riot.core.ReplicationType; import com.redis.riot.core.RiotStep; +import com.redis.riot.redis.CompareMode; +import com.redis.riot.redis.KeyComparisonStatusCountItemWriter; +import com.redis.riot.redis.Replication; +import com.redis.riot.redis.ReplicationMode; +import com.redis.riot.redis.ReplicationType; import com.redis.spring.batch.RedisItemReader; import com.redis.spring.batch.common.KeyComparison.Status; import com.redis.spring.batch.common.KeyComparisonItemReader; @@ -26,7 +26,7 @@ public class ReplicateCommand extends AbstractExportCommand { private static final Status[] STATUSES = { Status.OK, Status.MISSING, Status.TYPE, Status.VALUE, Status.TTL }; - private static final String QUEUE_MESSAGE = " | %,d queue space"; + private static final String QUEUE_MESSAGE = " | queue capacity: %,d"; private static final String NUMBER_FORMAT = "%,d"; private static final String COMPARE_MESSAGE = compareMessageFormat(); private static final Map taskNames = taskNames(); @@ -34,8 +34,8 @@ public class ReplicateCommand extends AbstractExportCommand { @Option(names = "--mode", description = "Replication mode (default: ${DEFAULT-VALUE}):%n SNAPSHOT: initial replication using key scan.%n LIVE: initial and continuous replication using key scan and keyspace notifications in parallel.%n LIVEONLY: continuous replication using keyspace notifications (only changed keys are replicated).%n COMPARE: compare source and target database.", paramLabel = "") ReplicationMode mode = ReplicationMode.SNAPSHOT; - @Option(names = "--type", description = "Replication strategy (default: ${DEFAULT-VALUE}):%n DUMP: dump & restore.%n STRUCT: type-based replication.", paramLabel = "") - ReplicationType type = ReplicationType.DUMP; + @Option(names = "--type", description = "Enable type-based replication") + boolean type; @Option(names = "--ttl-tolerance", description = "Max TTL offset in millis to use for dataset verification (default: ${DEFAULT-VALUE}).", paramLabel = "") long ttlTolerance = KeyComparisonItemReader.DEFAULT_TTL_TOLERANCE.toMillis(); @@ -64,7 +64,7 @@ private static Map taskNames() { } @Override - protected AbstractExport exportExecutable() { + protected AbstractExport exportRunnable() { Replication replication = new Replication(); replication.setCompareMode(compareMode); replication.setMode(mode); @@ -72,9 +72,9 @@ protected AbstractExport exportExecutable() { if (targetReadFrom != null) { replication.setTargetReadFrom(targetReadFrom.getReadFrom()); } - replication.setTargetRedisOptions(targetRedisArgs.redisOptions()); + replication.setTargetRedisClientOptions(targetRedisArgs.redisOptions()); replication.setTtlTolerance(Duration.ofMillis(ttlTolerance)); - replication.setType(type); + replication.setType(type ? ReplicationType.STRUCT : ReplicationType.DUMP); replication.setWriterOptions(writerArgs.writerOptions()); return replication; } diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/AbstractRedisCollectionCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/AbstractRedisCollectionCommand.java new file mode 100644 index 000000000..237030d1f --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/AbstractRedisCollectionCommand.java @@ -0,0 +1,29 @@ +package com.redis.riot.cli.redis; + +import java.util.List; + +import com.redis.riot.core.operation.AbstractCollectionMapOperationBuilder; +import com.redis.riot.core.operation.AbstractMapOperationBuilder; + +import picocli.CommandLine.Option; + +abstract class AbstractRedisCollectionCommand extends AbstractRedisCommand { + + @Option(names = "--member-space", description = "Keyspace prefix for member IDs.", paramLabel = "") + private String memberSpace; + + @Option(arity = "1..*", names = { "-m", + "--members" }, description = "Member field names for collections.", paramLabel = "") + private List memberFields; + + @Override + protected AbstractMapOperationBuilder operationBuilder() { + AbstractCollectionMapOperationBuilder builder = collectionOperationBuilder(); + builder.setMemberSpace(memberSpace); + builder.setMemberFields(memberFields); + return builder; + } + + protected abstract AbstractCollectionMapOperationBuilder collectionOperationBuilder(); + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/AbstractRedisCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/AbstractRedisCommand.java new file mode 100644 index 000000000..aa09a6f34 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/AbstractRedisCommand.java @@ -0,0 +1,44 @@ +package com.redis.riot.cli.redis; + +import java.util.List; +import java.util.Map; + +import com.redis.riot.cli.BaseCommand; +import com.redis.riot.cli.RedisCommand; +import com.redis.riot.core.operation.AbstractMapOperationBuilder; +import com.redis.spring.batch.common.Operation; + +import picocli.CommandLine.Option; + +abstract class AbstractRedisCommand extends BaseCommand implements RedisCommand { + + @Option(names = { "-p", "--keyspace" }, description = "Keyspace prefix.", paramLabel = "") + private String keyspace; + + @Option(names = { "-k", "--keys" }, arity = "1..*", description = "Key fields.", paramLabel = "") + private List keys; + + @Option(names = { "-s", + "--separator" }, description = "Key separator (default: ${DEFAULT-VALUE}).", paramLabel = "") + private String keySeparator = AbstractMapOperationBuilder.DEFAULT_SEPARATOR; + + @Option(names = { "-r", "--remove" }, description = "Remove key or member fields the first time they are used.") + private boolean removeFields = AbstractMapOperationBuilder.DEFAULT_REMOVE_FIELDS; + + @Option(names = "--ignore-missing", description = "Ignore missing fields.") + private boolean ignoreMissingFields = AbstractMapOperationBuilder.DEFAULT_IGNORE_MISSING_FIELDS; + + @Override + public Operation, Object> operation() { + AbstractMapOperationBuilder builder = operationBuilder(); + builder.setIgnoreMissingFields(ignoreMissingFields); + builder.setKeyFields(keys); + builder.setKeySeparator(keySeparator); + builder.setKeyspace(keyspace); + builder.setRemoveFields(removeFields); + return builder.build(); + } + + protected abstract AbstractMapOperationBuilder operationBuilder(); + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/DelCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/DelCommand.java new file mode 100644 index 000000000..4f991b20a --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/DelCommand.java @@ -0,0 +1,15 @@ +package com.redis.riot.cli.redis; + +import com.redis.riot.core.operation.DelBuilder; + +import picocli.CommandLine.Command; + +@Command(name = "del", description = "Delete keys") +public class DelCommand extends AbstractRedisCommand { + + @Override + protected DelBuilder operationBuilder() { + return new DelBuilder(); + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/ExpireCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/ExpireCommand.java new file mode 100644 index 000000000..7e738a8fc --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/ExpireCommand.java @@ -0,0 +1,29 @@ +package com.redis.riot.cli.redis; + +import java.time.Duration; + +import com.redis.riot.core.operation.ExpireBuilder; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command(name = "expire", description = "Set timeouts on keys") +public class ExpireCommand extends AbstractRedisCommand { + + public static final long DEFAULT_TTL = 60; + + @Option(names = "--ttl", description = "EXPIRE timeout field.", paramLabel = "") + private String ttlField; + + @Option(names = "--ttl-default", description = "EXPIRE default timeout (default: ${DEFAULT-VALUE}).", paramLabel = "") + private long defaultTtl = DEFAULT_TTL; + + @Override + protected ExpireBuilder operationBuilder() { + ExpireBuilder builder = new ExpireBuilder(); + builder.setTtlField(ttlField); + builder.setDefaultTtl(Duration.ofSeconds(defaultTtl)); + return builder; + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/FieldFilteringArgs.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/FieldFilteringArgs.java new file mode 100644 index 000000000..69297cd66 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/FieldFilteringArgs.java @@ -0,0 +1,22 @@ +package com.redis.riot.cli.redis; + +import java.util.List; + +import com.redis.riot.core.operation.AbstractFilterMapOperationBuilder; + +import picocli.CommandLine.Option; + +public class FieldFilteringArgs { + + @Option(arity = "1..*", names = "--include", description = "Fields to include.", paramLabel = "") + List includes; + + @Option(arity = "1..*", names = "--exclude", description = "Fields to exclude.", paramLabel = "") + List excludes; + + public void configure(AbstractFilterMapOperationBuilder builder) { + builder.setIncludes(includes); + builder.setExcludes(excludes); + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/GeoaddCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/GeoaddCommand.java new file mode 100644 index 000000000..e184a9b30 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/GeoaddCommand.java @@ -0,0 +1,25 @@ +package com.redis.riot.cli.redis; + +import com.redis.riot.core.operation.GeoaddBuilder; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command(name = "geoadd", description = "Add members to a geo set") +public class GeoaddCommand extends AbstractRedisCollectionCommand { + + @Option(names = "--lon", required = true, description = "Longitude field.", paramLabel = "") + private String longitude; + + @Option(names = "--lat", required = true, description = "Latitude field.", paramLabel = "") + private String latitude; + + @Override + protected GeoaddBuilder collectionOperationBuilder() { + GeoaddBuilder builder = new GeoaddBuilder(); + builder.setLatitude(latitude); + builder.setLongitude(longitude); + return builder; + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/HsetCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/HsetCommand.java new file mode 100644 index 000000000..babd80d7a --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/HsetCommand.java @@ -0,0 +1,21 @@ +package com.redis.riot.cli.redis; + +import com.redis.riot.core.operation.HsetBuilder; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = "hset", aliases = "hmset", description = "Set hashes from input") +public class HsetCommand extends AbstractRedisCommand { + + @Mixin + private FieldFilteringArgs filteringArgs = new FieldFilteringArgs(); + + @Override + protected HsetBuilder operationBuilder() { + HsetBuilder builder = new HsetBuilder(); + filteringArgs.configure(builder); + return builder; + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/JsonSetCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/JsonSetCommand.java new file mode 100644 index 000000000..62be063cf --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/JsonSetCommand.java @@ -0,0 +1,21 @@ +package com.redis.riot.cli.redis; + +import com.redis.riot.core.operation.JsonSetBuilder; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command(name = "json.set", description = "Add JSON documents to RedisJSON") +public class JsonSetCommand extends AbstractRedisCommand { + + @Option(names = "--path", description = "Path field.", paramLabel = "") + private String path; + + @Override + protected JsonSetBuilder operationBuilder() { + JsonSetBuilder supplier = new JsonSetBuilder(); + supplier.setPath(path); + return supplier; + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/LpushCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/LpushCommand.java new file mode 100644 index 000000000..f847056a0 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/LpushCommand.java @@ -0,0 +1,15 @@ +package com.redis.riot.cli.redis; + +import com.redis.riot.core.operation.LpushBuilder; + +import picocli.CommandLine.Command; + +@Command(name = "lpush", description = "Insert values at the head of a list") +public class LpushCommand extends AbstractRedisCollectionCommand { + + @Override + protected LpushBuilder collectionOperationBuilder() { + return new LpushBuilder(); + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/RpushCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/RpushCommand.java new file mode 100644 index 000000000..0a79be801 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/RpushCommand.java @@ -0,0 +1,15 @@ +package com.redis.riot.cli.redis; + +import com.redis.riot.core.operation.RpushBuilder; + +import picocli.CommandLine.Command; + +@Command(name = "rpush", description = "Insert values at the tail of a list") +public class RpushCommand extends AbstractRedisCollectionCommand { + + @Override + protected RpushBuilder collectionOperationBuilder() { + return new RpushBuilder(); + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/SaddCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/SaddCommand.java new file mode 100644 index 000000000..de293a534 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/SaddCommand.java @@ -0,0 +1,15 @@ +package com.redis.riot.cli.redis; + +import com.redis.riot.core.operation.SaddBuilder; + +import picocli.CommandLine.Command; + +@Command(name = "sadd", description = "Add members to a set") +public class SaddCommand extends AbstractRedisCollectionCommand { + + @Override + protected SaddBuilder collectionOperationBuilder() { + return new SaddBuilder(); + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/SetCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/SetCommand.java new file mode 100644 index 000000000..aaaae32a4 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/SetCommand.java @@ -0,0 +1,32 @@ +package com.redis.riot.cli.redis; + +import com.redis.riot.core.operation.SetBuilder; +import com.redis.riot.core.operation.SetBuilder.StringFormat; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command(name = "set", description = "Set strings from input") +public class SetCommand extends AbstractRedisCommand { + + public static final StringFormat DEFAULT_FORMAT = StringFormat.JSON; + + @Option(names = "--format", description = "Serialization: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE}).", paramLabel = "") + private StringFormat format = DEFAULT_FORMAT; + + @Option(names = "--field", description = "Raw value field.", paramLabel = "") + private String field; + + @Option(names = "--root", description = "XML root element name.", paramLabel = "") + private String root; + + @Override + protected SetBuilder operationBuilder() { + SetBuilder supplier = new SetBuilder(); + supplier.setField(field); + supplier.setFormat(format); + supplier.setRoot(root); + return supplier; + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/SugaddCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/SugaddCommand.java new file mode 100644 index 000000000..b5c8f0807 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/SugaddCommand.java @@ -0,0 +1,81 @@ +package com.redis.riot.cli.redis; + +import com.redis.riot.core.operation.SugaddBuilder; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command(name = "ft.sugadd", description = "Add suggestion strings to a RediSearch auto-complete dictionary") +public class SugaddCommand extends AbstractRedisCommand { + + public static final double DEFAULT_SCORE = 1; + + public static final boolean DEFAULT_INCREMENT = false; + + @Option(names = "--field", required = true, description = "Field containing the strings to add.", paramLabel = "") + private String stringField; + + @Option(names = "--score", description = "Name of the field to use for scores.", paramLabel = "") + private String scoreField; + + @Option(names = "--score-default", description = "Score when field not present (default: ${DEFAULT-VALUE}).", paramLabel = "") + private double defaultScore = DEFAULT_SCORE; + + @Option(names = "--payload", description = "Field containing the payload.", paramLabel = "") + private String payloadField; + + @Option(names = "--increment", description = "Increment the existing suggestion by the score instead of replacing the score.") + private boolean increment = DEFAULT_INCREMENT; + + public String getStringField() { + return stringField; + } + + public void setStringField(String field) { + this.stringField = field; + } + + public String getScoreField() { + return scoreField; + } + + public void setScore(String field) { + this.scoreField = field; + } + + public double getDefaultScore() { + return defaultScore; + } + + public void setDefaultScore(double scoreDefault) { + this.defaultScore = scoreDefault; + } + + public String getPayloadField() { + return payloadField; + } + + public void setPayload(String field) { + this.payloadField = field; + } + + public boolean isIncrement() { + return increment; + } + + public void setIncrement(boolean increment) { + this.increment = increment; + } + + @Override + protected SugaddBuilder operationBuilder() { + SugaddBuilder supplier = new SugaddBuilder(); + supplier.setDefaultScore(defaultScore); + supplier.setIncrement(increment); + supplier.setStringField(stringField); + supplier.setPayloadField(payloadField); + supplier.setScoreField(scoreField); + return supplier; + } + +} diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/TsAddCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/TsAddCommand.java new file mode 100644 index 000000000..20211096a --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/TsAddCommand.java @@ -0,0 +1,37 @@ +package com.redis.riot.cli.redis; + +import java.util.LinkedHashMap; +import java.util.Map; + +import com.redis.lettucemod.timeseries.DuplicatePolicy; +import com.redis.riot.core.operation.TsAddBuilder; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command(name = "ts.add", description = "Add samples to RedisTimeSeries") +public class TsAddCommand extends AbstractRedisCommand { + + @Option(names = "--timestamp", description = "Name of the field to use for timestamps. If unset, uses auto-timestamping.", paramLabel = "") + private String timestampField; + + @Option(names = "--value", required = true, description = "Name of the field to use for values.", paramLabel = "") + private String valueField; + + @Option(names = "--on-duplicate", description = "Duplicate policy: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE}).", paramLabel = "") + private DuplicatePolicy duplicatePolicy = TsAddBuilder.DEFAULT_DUPLICATE_POLICY; + + @Option(arity = "1..*", names = "--labels", description = "Labels in the form label1=field1 label2=field2...", paramLabel = "SPEL") + private Map labels = new LinkedHashMap<>(); + + @Override + protected TsAddBuilder operationBuilder() { + TsAddBuilder supplier = new TsAddBuilder(); + supplier.setDuplicatePolicy(duplicatePolicy); + supplier.setTimestampField(timestampField); + supplier.setValueField(valueField); + supplier.setLabels(labels); + return supplier; + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/XaddCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/XaddCommand.java new file mode 100644 index 000000000..56d3d6674 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/XaddCommand.java @@ -0,0 +1,53 @@ +package com.redis.riot.cli.redis; + +import com.redis.riot.core.operation.XaddSupplier; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +@Command(name = "xadd", description = "Append entries to a stream") +public class XaddCommand extends AbstractRedisCommand { + + @Mixin + private FieldFilteringArgs filteringOptions = new FieldFilteringArgs(); + + @Option(names = "--maxlen", description = "Stream maxlen.", paramLabel = "") + private long maxlen; + + @Option(names = "--trim", description = "Stream efficient trimming ('~' flag).") + private boolean approximateTrimming; + + public FieldFilteringArgs getFilteringOptions() { + return filteringOptions; + } + + public void setFilteringOptions(FieldFilteringArgs filteringOptions) { + this.filteringOptions = filteringOptions; + } + + public long getMaxlen() { + return maxlen; + } + + public void setMaxlen(long maxlen) { + this.maxlen = maxlen; + } + + public boolean isApproximateTrimming() { + return approximateTrimming; + } + + public void setApproximateTrimming(boolean approximateTrimming) { + this.approximateTrimming = approximateTrimming; + } + + @Override + protected XaddSupplier operationBuilder() { + XaddSupplier supplier = new XaddSupplier(); + supplier.setApproximateTrimming(approximateTrimming); + supplier.setMaxlen(maxlen); + return supplier; + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/java/com/redis/riot/cli/redis/ZaddCommand.java b/plugins/riot/src/main/java/com/redis/riot/cli/redis/ZaddCommand.java new file mode 100644 index 000000000..8b241bbd9 --- /dev/null +++ b/plugins/riot/src/main/java/com/redis/riot/cli/redis/ZaddCommand.java @@ -0,0 +1,25 @@ +package com.redis.riot.cli.redis; + +import com.redis.riot.core.operation.ZaddSupplier; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command(name = "zadd", description = "Add members with scores to a sorted set") +public class ZaddCommand extends AbstractRedisCollectionCommand { + + @Option(names = "--score", description = "Name of the field to use for scores.", paramLabel = "") + private String scoreField; + + @Option(names = "--score-default", description = "Score when field not present (default: ${DEFAULT-VALUE}).", paramLabel = "") + private double defaultScore = ZaddSupplier.DEFAULT_SCORE; + + @Override + protected ZaddSupplier collectionOperationBuilder() { + ZaddSupplier supplier = new ZaddSupplier(); + supplier.setScoreField(scoreField); + supplier.setDefaultScore(defaultScore); + return supplier; + } + +} \ No newline at end of file diff --git a/plugins/riot/src/main/resources/com/redis/riot/Messages.properties b/plugins/riot/src/main/resources/com/redis/riot/Messages.properties deleted file mode 100644 index 89afb8a89..000000000 --- a/plugins/riot/src/main/resources/com/redis/riot/Messages.properties +++ /dev/null @@ -1,15 +0,0 @@ -# Header -usage.headerHeading = RIOT is a data import/export tool for Redis.%n%n -# Footer -usage.synopsisHeading = %nUsage:\u0020 -usage.optionListHeading = %nOptions:%n -usage.commandListHeading = %nCommands:%n -usage.footerHeading = %nDocumentation found at https://developer.redis.com/riot%n -helpCommand.command = COMMAND -help = Show this help message and exit. -version = Print version information and exit. -# options -debug = Set log level to debug. -info = Set log level to info. -warn = Set log level to warn. -quiet = Log errors only. \ No newline at end of file diff --git a/plugins/riot/src/test/java/com/redis/riot/cli/AbstractDbTests.java b/plugins/riot/src/test/java/com/redis/riot/cli/AbstractDbTests.java index 93390dc68..8e98afa7a 100644 --- a/plugins/riot/src/test/java/com/redis/riot/cli/AbstractDbTests.java +++ b/plugins/riot/src/test/java/com/redis/riot/cli/AbstractDbTests.java @@ -15,8 +15,6 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.testcontainers.containers.JdbcDatabaseContainer; -import com.redis.spring.batch.common.DataType; -import com.redis.spring.batch.test.AbstractTestBase; import com.redis.testcontainers.RedisServer; import com.redis.testcontainers.RedisStackContainer; @@ -25,77 +23,74 @@ abstract class AbstractDbTests extends AbstractRiotTestBase { - private static final RedisStackContainer REDIS = new RedisStackContainer( - RedisStackContainer.DEFAULT_IMAGE_NAME.withTag(RedisStackContainer.DEFAULT_TAG)); - - protected abstract JdbcDatabaseContainer getJdbcDatabaseContainer(); - - protected Connection databaseConnection; - - protected DataSource dataSource; - - @Override - protected DataType[] generatorDataTypes() { - return AbstractTestBase.REDIS_MODULES_GENERATOR_TYPES; - } - - @BeforeAll - public void setupContainers() throws SQLException { - JdbcDatabaseContainer container = getJdbcDatabaseContainer(); - container.start(); - DataSourceProperties properties = new DataSourceProperties(); - properties.setUrl(container.getJdbcUrl()); - properties.setUsername(container.getUsername()); - properties.setPassword(container.getPassword()); - dataSource = properties.initializeDataSourceBuilder().build(); - databaseConnection = dataSource.getConnection(); - } - - @AfterAll - public void teardownContainers() throws SQLException { - databaseConnection.close(); - getJdbcDatabaseContainer().stop(); - } - - @Override - protected RedisServer getRedisServer() { - return REDIS; - } - - @Override - protected RedisStackContainer getTargetRedisServer() { - return REDIS; - } - - protected void executeScript(String file) throws IOException, SQLException { - SqlScriptRunner scriptRunner = new SqlScriptRunner(databaseConnection); - scriptRunner.setAutoCommit(false); - scriptRunner.setStopOnError(true); - InputStream inputStream = PostgresTests.class.getClassLoader().getResourceAsStream(file); - if (inputStream == null) { - throw new FileNotFoundException(file); - } - scriptRunner.runScript(new InputStreamReader(inputStream)); - } - - protected int executeDatabaseImport(ParseResult parseResult) { - DatabaseImportCommand command = command(parseResult); - configureDatabase(command.args); - return ExitCode.OK; - } - - protected int executeDatabaseExport(ParseResult parseResult, TestInfo info) { - DatabaseExportCommand command = command(parseResult); - command.setName(name(info)); - configureDatabase(command.args); - return ExitCode.OK; - } - - private void configureDatabase(DatabaseArgs args) { - JdbcDatabaseContainer container = getJdbcDatabaseContainer(); - args.url = container.getJdbcUrl(); - args.username = container.getUsername(); - args.password = container.getPassword(); - } + private static final RedisStackContainer redis = new RedisStackContainer( + RedisStackContainer.DEFAULT_IMAGE_NAME.withTag(RedisStackContainer.DEFAULT_TAG)); + + protected abstract JdbcDatabaseContainer getJdbcDatabaseContainer(); + + protected Connection dbConnection; + + protected DataSource dataSource; + + @BeforeAll + public void setupContainers() throws SQLException { + JdbcDatabaseContainer container = getJdbcDatabaseContainer(); + container.start(); + DataSourceProperties properties = new DataSourceProperties(); + properties.setUrl(container.getJdbcUrl()); + properties.setUsername(container.getUsername()); + properties.setPassword(container.getPassword()); + dataSource = properties.initializeDataSourceBuilder().build(); + dbConnection = dataSource.getConnection(); + } + + @AfterAll + public void teardownContainers() throws SQLException { + if (dbConnection != null) { + dbConnection.close(); + } + getJdbcDatabaseContainer().stop(); + } + + @Override + protected RedisServer getRedisServer() { + return redis; + } + + @Override + protected RedisStackContainer getTargetRedisServer() { + return redis; + } + + protected void executeScript(String file) throws IOException, SQLException { + SqlScriptRunner scriptRunner = new SqlScriptRunner(dbConnection); + scriptRunner.setAutoCommit(false); + scriptRunner.setStopOnError(true); + InputStream inputStream = PostgresTests.class.getClassLoader().getResourceAsStream(file); + if (inputStream == null) { + throw new FileNotFoundException(file); + } + scriptRunner.runScript(new InputStreamReader(inputStream)); + } + + protected int executeDatabaseImport(ParseResult parseResult) { + DatabaseImportCommand command = command(parseResult); + configureDatabase(command.args); + return ExitCode.OK; + } + + protected int executeDatabaseExport(ParseResult parseResult, TestInfo info) { + DatabaseExportCommand command = command(parseResult); + command.setName(name(info)); + configureDatabase(command.args); + return ExitCode.OK; + } + + private void configureDatabase(DatabaseArgs args) { + JdbcDatabaseContainer container = getJdbcDatabaseContainer(); + args.url = container.getJdbcUrl(); + args.username = container.getUsername(); + args.password = container.getPassword(); + } } diff --git a/plugins/riot/src/test/java/com/redis/riot/cli/AbstractIntegrationTests.java b/plugins/riot/src/test/java/com/redis/riot/cli/AbstractIntegrationTests.java index d966c42ed..c28b4124d 100644 --- a/plugins/riot/src/test/java/com/redis/riot/cli/AbstractIntegrationTests.java +++ b/plugins/riot/src/test/java/com/redis/riot/cli/AbstractIntegrationTests.java @@ -37,10 +37,10 @@ import com.redis.lettucemod.timeseries.MRangeOptions; import com.redis.lettucemod.timeseries.RangeResult; import com.redis.lettucemod.timeseries.TimeRange; -import com.redis.riot.core.GeneratorImport; import com.redis.riot.file.resource.XmlItemReader; import com.redis.riot.file.resource.XmlItemReaderBuilder; import com.redis.riot.file.resource.XmlObjectReader; +import com.redis.riot.redis.GeneratorImport; import com.redis.spring.batch.common.KeyValue; import com.redis.spring.batch.gen.GeneratorItemReader; @@ -344,7 +344,7 @@ void fileExportJSONGz(TestInfo info) throws Exception { private List exportToJsonFile(TestInfo info) throws Exception { String filename = "file-export-json"; Path file = tempFile("redis.json"); - generate(info); + generate(info, generator(73)); execute(info, filename, r -> executeFileDumpExport(r, info)); JsonItemReaderBuilder builder = new JsonItemReaderBuilder<>(); builder.name("json-data-structure-file-reader"); @@ -365,7 +365,7 @@ private List exportToJsonFile(TestInfo info) throws Exception { @Test void fileExportXml(TestInfo info) throws Exception { String filename = "file-export-xml"; - generate(info); + generate(info, generator(73)); Path file = tempFile("redis.xml"); execute(info, filename, r -> executeFileDumpExport(r, info)); XmlItemReaderBuilder builder = new XmlItemReaderBuilder<>(); diff --git a/plugins/riot/src/test/java/com/redis/riot/cli/AbstractReplicationTests.java b/plugins/riot/src/test/java/com/redis/riot/cli/AbstractReplicationTests.java index ce08b0a50..7bd66e180 100644 --- a/plugins/riot/src/test/java/com/redis/riot/cli/AbstractReplicationTests.java +++ b/plugins/riot/src/test/java/com/redis/riot/cli/AbstractReplicationTests.java @@ -35,7 +35,7 @@ void setDefaults() { @Test void replicate(TestInfo info) throws Throwable { String filename = "replicate"; - generate(info); + generate(info, generator(73)); Assertions.assertTrue(commands.dbsize() > 0); execute(info, filename); } @@ -43,7 +43,7 @@ void replicate(TestInfo info) throws Throwable { @Test void replicateDryRun(TestInfo info) throws Throwable { String filename = "replicate-dry-run"; - generate(info); + generate(info, generator(73)); Assertions.assertTrue(commands.dbsize() > 0); execute(info, filename); Assertions.assertEquals(0, targetCommands.dbsize()); @@ -155,10 +155,9 @@ void replicateStruct(TestInfo info) throws Throwable { } protected void runLiveReplication(TestInfo info, String filename) throws Exception { - DataType[] types = new DataType[] { DataType.HASH, DataType.STRING, DataType.LIST, DataType.ZSET }; + DataType[] types = new DataType[] { DataType.HASH, DataType.STRING }; enableKeyspaceNotifications(client); - GeneratorItemReader gen = generator(3000, types); - generate(info, gen); + generate(info, generator(3000, types)); Executors.newSingleThreadScheduledExecutor().execute(() -> { awaitPubSub(); GeneratorItemReader generator = generator(3500, types); diff --git a/plugins/riot/src/test/java/com/redis/riot/cli/AbstractRiotTestBase.java b/plugins/riot/src/test/java/com/redis/riot/cli/AbstractRiotTestBase.java index 6aa4564a0..f8872fecb 100644 --- a/plugins/riot/src/test/java/com/redis/riot/cli/AbstractRiotTestBase.java +++ b/plugins/riot/src/test/java/com/redis/riot/cli/AbstractRiotTestBase.java @@ -3,15 +3,15 @@ import java.io.InputStream; import java.io.PrintWriter; import java.nio.charset.Charset; +import java.util.StringTokenizer; +import java.util.Vector; -import org.codehaus.plexus.util.cli.CommandLineUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.TestInfo; import org.slf4j.simple.SimpleLogger; -import com.redis.riot.cli.AbstractImportCommand.OperationCommand; import com.redis.riot.cli.AbstractJobCommand.ProgressStyle; -import com.redis.riot.core.ReplicationMode; +import com.redis.riot.redis.ReplicationMode; import com.redis.spring.batch.test.AbstractTargetTestBase; import com.redis.testcontainers.RedisServer; @@ -43,7 +43,7 @@ protected T command(ParseResult parseResult) { protected int execute(TestInfo info, String filename, IExecutionStrategy... executionStrategies) throws Exception { String[] args = args(filename); IExecutionStrategy executionStrategy = executionStrategy(info, filename, executionStrategies); - return Main.run(out, err, args, executionStrategy); + return AbstractMainCommand.run(new Main(), out, err, args, executionStrategy); } private IExecutionStrategy executionStrategy(TestInfo info, String name, @@ -56,12 +56,12 @@ private IExecutionStrategy executionStrategy(TestInfo info, String name, private int execute(TestInfo info, String name, ParseResult parseResult) { RedisServer server = getRedisServer(); - Main main = (Main) parseResult.commandSpec().commandLine().getCommand(); + AbstractMainCommand main = (AbstractMainCommand) parseResult.commandSpec().commandLine().getCommand(); main.redisArgs.uri = server.getRedisURI(); main.redisArgs.cluster = server.isRedisCluster(); for (ParseResult subParseResult : parseResult.subcommands()) { Object command = subParseResult.commandSpec().commandLine().getCommand(); - if (command instanceof OperationCommand) { + if (command instanceof RedisCommand) { command = subParseResult.commandSpec().parent().commandLine().getCommand(); } if (command instanceof AbstractJobCommand) { @@ -83,13 +83,75 @@ private int execute(TestInfo info, String name, ParseResult parseResult) { } private static String[] args(String filename) throws Exception { - try (InputStream inputStream = Main.class.getResourceAsStream("/" + filename)) { + try (InputStream inputStream = AbstractMainCommand.class.getResourceAsStream("/" + filename)) { String command = IOUtils.toString(inputStream, Charset.defaultCharset()); if (command.startsWith(PREFIX)) { command = command.substring(PREFIX.length()); } - return CommandLineUtils.translateCommandline(command); + return translateCommandline(command); } } + private static String[] translateCommandline(String toProcess) throws Exception { + if ((toProcess == null) || (toProcess.length() == 0)) { + return new String[0]; + } + + // parse with a simple finite state machine + + final int normal = 0; + final int inQuote = 1; + final int inDoubleQuote = 2; + int state = normal; + StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true); + Vector v = new Vector(); + StringBuilder current = new StringBuilder(); + + while (tok.hasMoreTokens()) { + String nextTok = tok.nextToken(); + switch (state) { + case inQuote: + if ("\'".equals(nextTok)) { + state = normal; + } else { + current.append(nextTok); + } + break; + case inDoubleQuote: + if ("\"".equals(nextTok)) { + state = normal; + } else { + current.append(nextTok); + } + break; + default: + if ("\'".equals(nextTok)) { + state = inQuote; + } else if ("\"".equals(nextTok)) { + state = inDoubleQuote; + } else if (" ".equals(nextTok)) { + if (current.length() != 0) { + v.addElement(current.toString()); + current.setLength(0); + } + } else { + current.append(nextTok); + } + break; + } + } + + if (current.length() != 0) { + v.addElement(current.toString()); + } + + if ((state == inQuote) || (state == inDoubleQuote)) { + throw new Exception("unbalanced quotes in " + toProcess); + } + + String[] args = new String[v.size()]; + v.copyInto(args); + return args; + } + } diff --git a/plugins/riot/src/test/java/com/redis/riot/cli/EnterpriseServerToStackTests.java b/plugins/riot/src/test/java/com/redis/riot/cli/EnterpriseServerToStackTests.java index 667a0bc6e..826a6ce18 100644 --- a/plugins/riot/src/test/java/com/redis/riot/cli/EnterpriseServerToStackTests.java +++ b/plugins/riot/src/test/java/com/redis/riot/cli/EnterpriseServerToStackTests.java @@ -2,8 +2,6 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import com.redis.spring.batch.common.DataType; -import com.redis.spring.batch.test.AbstractTestBase; import com.redis.testcontainers.RedisEnterpriseServer; import com.redis.testcontainers.RedisStackContainer; @@ -11,7 +9,6 @@ class EnterpriseServerToStackTests extends AbstractIntegrationTests { private static final RedisEnterpriseServer source = RedisContainerFactory.enterpriseServer(); - private static final RedisStackContainer target = RedisContainerFactory.stack(); @Override @@ -24,9 +21,4 @@ protected RedisStackContainer getTargetRedisServer() { return target; } - @Override - protected DataType[] generatorDataTypes() { - return AbstractTestBase.REDIS_MODULES_GENERATOR_TYPES; - } - } diff --git a/plugins/riot/src/test/java/com/redis/riot/cli/EnterpriseToStackTests.java b/plugins/riot/src/test/java/com/redis/riot/cli/EnterpriseToStackTests.java index 0b9f14a44..738d3889f 100644 --- a/plugins/riot/src/test/java/com/redis/riot/cli/EnterpriseToStackTests.java +++ b/plugins/riot/src/test/java/com/redis/riot/cli/EnterpriseToStackTests.java @@ -3,8 +3,6 @@ import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; -import com.redis.spring.batch.common.DataType; -import com.redis.spring.batch.test.AbstractTestBase; import com.redis.testcontainers.RedisEnterpriseContainer; import com.redis.testcontainers.RedisStackContainer; @@ -12,7 +10,6 @@ class EnterpriseContainerToStackTests extends AbstractIntegrationTests { private static final RedisEnterpriseContainer source = RedisContainerFactory.enterprise(); - private static final RedisStackContainer target = RedisContainerFactory.stack(); @Override @@ -25,9 +22,4 @@ protected RedisStackContainer getTargetRedisServer() { return target; } - @Override - protected DataType[] generatorDataTypes() { - return AbstractTestBase.REDIS_MODULES_GENERATOR_TYPES; - } - } diff --git a/plugins/riot/src/test/java/com/redis/riot/cli/PostgresTests.java b/plugins/riot/src/test/java/com/redis/riot/cli/PostgresTests.java index 74f881e35..b33437d26 100644 --- a/plugins/riot/src/test/java/com/redis/riot/cli/PostgresTests.java +++ b/plugins/riot/src/test/java/com/redis/riot/cli/PostgresTests.java @@ -25,14 +25,13 @@ class PostgresTests extends AbstractDbTests { - private static final DockerImageName POSTGRES_IMAGE = DockerImageName.parse(PostgreSQLContainer.IMAGE) + private static final DockerImageName postgresImage = DockerImageName.parse(PostgreSQLContainer.IMAGE) .withTag(PostgreSQLContainer.DEFAULT_TAG); - - private static final PostgreSQLContainer POSTGRES = new PostgreSQLContainer<>(POSTGRES_IMAGE); + private static final PostgreSQLContainer postgres = new PostgreSQLContainer<>(postgresImage); @Override protected JdbcDatabaseContainer getJdbcDatabaseContainer() { - return POSTGRES; + return postgres; } @BeforeAll @@ -42,7 +41,7 @@ void setupPostgres() throws SQLException, IOException { @BeforeEach void clearTables() throws SQLException { - try (Statement statement = databaseConnection.createStatement()) { + try (Statement statement = dbConnection.createStatement()) { statement.execute("DROP TABLE IF EXISTS mytable"); } } @@ -50,11 +49,10 @@ void clearTables() throws SQLException { @Test void export(TestInfo info) throws Exception { String filename = "db-export-postgresql"; - try (Statement statement = databaseConnection.createStatement()) { + try (Statement statement = dbConnection.createStatement()) { statement.execute("CREATE TABLE mytable (id smallint NOT NULL, field1 bpchar, field2 bpchar)"); statement.execute("ALTER TABLE ONLY mytable ADD CONSTRAINT pk_mytable PRIMARY KEY (id)"); - GeneratorItemReader generator = generator(); - generator.setTypes(DataType.HASH); + GeneratorItemReader generator = generator(73, DataType.HASH); generate(info, generator); execute(info, filename, r -> executeDatabaseExport(r, info)); statement.execute("SELECT COUNT(*) AS count FROM mytable"); @@ -75,7 +73,7 @@ void export(TestInfo info) throws Exception { @Test void nullValueExport(TestInfo info) throws Exception { - try (Statement statement = databaseConnection.createStatement()) { + try (Statement statement = dbConnection.createStatement()) { statement.execute("CREATE TABLE mytable (id smallint NOT NULL, field1 bpchar, field2 bpchar)"); statement.execute("ALTER TABLE ONLY mytable ADD CONSTRAINT pk_mytable PRIMARY KEY (id)"); Map hash1 = new HashMap<>(); @@ -104,7 +102,7 @@ void nullValueExport(TestInfo info) throws Exception { @Test void hashImport(TestInfo info) throws Exception { execute(info, "db-import-postgresql", this::executeDatabaseImport); - try (Statement statement = databaseConnection.createStatement()) { + try (Statement statement = dbConnection.createStatement()) { statement.execute("SELECT COUNT(*) AS count FROM orders"); ResultSet resultSet = statement.getResultSet(); resultSet.next(); @@ -119,7 +117,7 @@ void hashImport(TestInfo info) throws Exception { void multiThreadedImport(TestInfo info) throws Exception { execute(info, "db-import-postgresql-multithreaded", this::executeDatabaseImport); int count = keyCount("order:*"); - try (Statement statement = databaseConnection.createStatement()) { + try (Statement statement = dbConnection.createStatement()) { try (ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) AS count FROM orders")) { Assertions.assertTrue(resultSet.next()); Assertions.assertEquals(resultSet.getLong("count"), count); @@ -133,7 +131,7 @@ void multiThreadedImport(TestInfo info) throws Exception { @Test void setImport(TestInfo info) throws Exception { execute(info, "db-import-postgresql-set", this::executeDatabaseImport); - try (Statement statement = databaseConnection.createStatement()) { + try (Statement statement = dbConnection.createStatement()) { statement.execute("SELECT * FROM orders"); ResultSet resultSet = statement.getResultSet(); long count = 0; diff --git a/plugins/riot/src/test/java/com/redis/riot/cli/StackToEnterpriseContainerTests.java b/plugins/riot/src/test/java/com/redis/riot/cli/StackToEnterpriseContainerTests.java index 9a1843b87..ad5ec0239 100644 --- a/plugins/riot/src/test/java/com/redis/riot/cli/StackToEnterpriseContainerTests.java +++ b/plugins/riot/src/test/java/com/redis/riot/cli/StackToEnterpriseContainerTests.java @@ -3,31 +3,23 @@ import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; -import com.redis.spring.batch.common.DataType; -import com.redis.spring.batch.test.AbstractTestBase; import com.redis.testcontainers.RedisEnterpriseContainer; import com.redis.testcontainers.RedisStackContainer; @EnabledOnOs(OS.LINUX) class StackToEnterpriseContainerTests extends AbstractIntegrationTests { - private static final RedisStackContainer source = RedisContainerFactory.stack(); + private static final RedisStackContainer source = RedisContainerFactory.stack(); + private static final RedisEnterpriseContainer target = RedisContainerFactory.enterprise(); - private static final RedisEnterpriseContainer target = RedisContainerFactory.enterprise(); + @Override + protected RedisStackContainer getRedisServer() { + return source; + } - @Override - protected RedisStackContainer getRedisServer() { - return source; - } - - @Override - protected RedisEnterpriseContainer getTargetRedisServer() { - return target; - } - - @Override - protected DataType[] generatorDataTypes() { - return AbstractTestBase.REDIS_MODULES_GENERATOR_TYPES; - } + @Override + protected RedisEnterpriseContainer getTargetRedisServer() { + return target; + } } diff --git a/plugins/riot/src/test/java/com/redis/riot/cli/StackToEnterpriseServerTests.java b/plugins/riot/src/test/java/com/redis/riot/cli/StackToEnterpriseServerTests.java index a6f1ebedf..3b5df6141 100644 --- a/plugins/riot/src/test/java/com/redis/riot/cli/StackToEnterpriseServerTests.java +++ b/plugins/riot/src/test/java/com/redis/riot/cli/StackToEnterpriseServerTests.java @@ -2,8 +2,6 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import com.redis.spring.batch.common.DataType; -import com.redis.spring.batch.test.AbstractTestBase; import com.redis.testcontainers.RedisEnterpriseServer; import com.redis.testcontainers.RedisStackContainer; @@ -11,7 +9,6 @@ class StackToEnterpriseServerTests extends AbstractIntegrationTests { private static final RedisStackContainer source = RedisContainerFactory.stack(); - private static final RedisEnterpriseServer target = RedisContainerFactory.enterpriseServer(); @Override @@ -24,9 +21,4 @@ protected RedisEnterpriseServer getTargetRedisServer() { return target; } - @Override - protected DataType[] generatorDataTypes() { - return AbstractTestBase.REDIS_MODULES_GENERATOR_TYPES; - } - } diff --git a/plugins/riot/src/test/java/com/redis/riot/cli/StackToStackIntegrationTests.java b/plugins/riot/src/test/java/com/redis/riot/cli/StackToStackIntegrationTests.java index ec2fa25f1..f2c97407b 100644 --- a/plugins/riot/src/test/java/com/redis/riot/cli/StackToStackIntegrationTests.java +++ b/plugins/riot/src/test/java/com/redis/riot/cli/StackToStackIntegrationTests.java @@ -1,28 +1,20 @@ package com.redis.riot.cli; -import com.redis.spring.batch.common.DataType; -import com.redis.spring.batch.test.AbstractTestBase; import com.redis.testcontainers.RedisStackContainer; class StackToStackIntegrationTests extends AbstractIntegrationTests { - private static final RedisStackContainer source = RedisContainerFactory.stack(); + private static final RedisStackContainer source = RedisContainerFactory.stack(); + private static final RedisStackContainer target = RedisContainerFactory.stack(); - private static final RedisStackContainer target = RedisContainerFactory.stack(); + @Override + protected RedisStackContainer getRedisServer() { + return source; + } - @Override - protected RedisStackContainer getRedisServer() { - return source; - } - - @Override - protected RedisStackContainer getTargetRedisServer() { - return target; - } - - @Override - protected DataType[] generatorDataTypes() { - return AbstractTestBase.REDIS_MODULES_GENERATOR_TYPES; - } + @Override + protected RedisStackContainer getTargetRedisServer() { + return target; + } } diff --git a/plugins/riot/src/test/java/com/redis/riot/cli/StackToStackReplicationTests.java b/plugins/riot/src/test/java/com/redis/riot/cli/StackToStackReplicationTests.java index f75210e79..82074b21f 100644 --- a/plugins/riot/src/test/java/com/redis/riot/cli/StackToStackReplicationTests.java +++ b/plugins/riot/src/test/java/com/redis/riot/cli/StackToStackReplicationTests.java @@ -1,13 +1,10 @@ package com.redis.riot.cli; -import com.redis.spring.batch.common.DataType; -import com.redis.spring.batch.test.AbstractTestBase; import com.redis.testcontainers.RedisStackContainer; class StackToStackReplicationTests extends AbstractReplicationTests { private static final RedisStackContainer source = RedisContainerFactory.stack(); - private static final RedisStackContainer target = RedisContainerFactory.stack(); @Override @@ -20,9 +17,4 @@ protected RedisStackContainer getTargetRedisServer() { return target; } - @Override - protected DataType[] generatorDataTypes() { - return AbstractTestBase.REDIS_MODULES_GENERATOR_TYPES; - } - } diff --git a/plugins/riot/src/test/resources/replicate-hll b/plugins/riot/src/test/resources/replicate-hll index 2380e1c0d..e63b40ca2 100644 --- a/plugins/riot/src/test/resources/replicate-hll +++ b/plugins/riot/src/test/resources/replicate-hll @@ -1 +1 @@ -riot -h source -p 6379 replicate --type struct -h target -p 6380 --batch 10 \ No newline at end of file +riot -h source -p 6379 replicate --type -h target -p 6380 --batch 10 \ No newline at end of file diff --git a/plugins/riot/src/test/resources/replicate-live-struct b/plugins/riot/src/test/resources/replicate-live-struct index da0f38cb5..9e91ecacf 100644 --- a/plugins/riot/src/test/resources/replicate-live-struct +++ b/plugins/riot/src/test/resources/replicate-live-struct @@ -1 +1 @@ -riot -h source -p 6379 replicate -h target -p 6380 --type struct --mode live --compare none \ No newline at end of file +riot -h source -p 6379 replicate -h target -p 6380 --type --mode live --compare none \ No newline at end of file diff --git a/plugins/riot/src/test/resources/replicate-struct b/plugins/riot/src/test/resources/replicate-struct index c7891bb23..77c8d5460 100644 --- a/plugins/riot/src/test/resources/replicate-struct +++ b/plugins/riot/src/test/resources/replicate-struct @@ -1 +1 @@ -riot -h source -p 6379 replicate -h target -p 6380 --type struct \ No newline at end of file +riot -h source -p 6379 replicate -h target -p 6380 --type \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 09f630ba8..1d5a2dd1d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -49,8 +49,6 @@ projects { } dirs(['core', 'connectors', 'plugins']) { id 'java-library' - } - dirs(['core', 'connectors']) { id 'org.springframework.boot' id 'io.spring.dependency-management' }