From 8ed22e8b967287754e78a6465138dab6ada1770c Mon Sep 17 00:00:00 2001 From: Anton Duyun Date: Tue, 11 Feb 2025 19:04:29 +0300 Subject: [PATCH 1/2] rewrite s3 client to use kora http client interface --- .../processor/common/CommonClassNames.java | 2 + common/src/main/java/module-info.java | 16 + .../main/java/ru/tinkoff/kora/common/Tag.java | 1 + .../kora/common/annotation/Generated.java | 3 +- .../processor/ConfigParserGenerator.java | 5 +- .../src/main/java/module-info.java | 9 + .../kora/database/symbol/processor/DbUtils.kt | 2 +- .../build.gradle | 3 +- .../annotation/processor/S3ClassNames.java | 39 + .../S3ClientAnnotationProcessor.java | 1331 ++++++----------- .../annotation/processor/S3Operation.java | 32 - .../annotation/processor/AbstractS3Test.java | 42 + .../processor/S3AwsAsyncClientTests.java | 207 --- .../processor/S3AwsClientTests.java | 192 --- .../processor/S3AwsReactorClientTests.java | 148 -- .../annotation/processor/S3DeleteTest.java | 66 + .../annotation/processor/S3GetTest.java | 364 +++++ .../processor/S3KoraAsyncClientTests.java | 644 -------- .../processor/S3KoraClientTests.java | 611 -------- .../processor/S3KoraReactorClientTests.java | 553 ------- .../annotation/processor/S3ListTest.java | 157 ++ .../annotation/processor/S3PutTest.java | 54 + experimental/s3-client-aws/build.gradle | 20 - .../kora/s3/client/aws/AwsS3BodyAsync.java | 57 - .../kora/s3/client/aws/AwsS3BodySync.java | 58 - .../kora/s3/client/aws/AwsS3ClientConfig.java | 47 - .../kora/s3/client/aws/AwsS3ClientModule.java | 139 -- .../aws/AwsS3ClientTelemetryInterceptor.java | 145 -- .../s3/client/aws/AwsS3KoraAsyncClient.java | 344 ----- .../kora/s3/client/aws/AwsS3KoraClient.java | 317 ---- .../kora/s3/client/aws/AwsS3Object.java | 81 - .../kora/s3/client/aws/AwsS3ObjectList.java | 42 - .../kora/s3/client/aws/AwsS3ObjectMeta.java | 72 - .../s3/client/aws/AwsS3ObjectMetaList.java | 43 - .../kora/s3/client/aws/AwsS3ObjectUpload.java | 13 - .../s3/client/aws/KoraAwsSdkHttpClient.java | 195 --- .../kora/s3/client/aws/package-info.java | 4 - .../kora/s3/client/S3ClientConfig.java | 12 - .../kora/s3/client/S3ClientModule.java | 41 - .../ru/tinkoff/kora/s3/client/S3Config.java | 23 - .../kora/s3/client/S3DeleteException.java | 27 - .../tinkoff/kora/s3/client/S3Exception.java | 30 - .../kora/s3/client/S3KoraAsyncClient.java | 76 - .../tinkoff/kora/s3/client/S3KoraClient.java | 75 - .../kora/s3/client/S3NotFoundException.java | 19 - .../s3/client/model/ByteBufferPublisher.java | 51 - .../kora/s3/client/model/ByteS3Body.java | 38 - .../InputStreamByteBufferSubscriber.java | 231 --- .../s3/client/model/InputStreamS3Body.java | 32 - .../kora/s3/client/model/PublisherS3Body.java | 33 - .../tinkoff/kora/s3/client/model/S3Body.java | 123 -- .../kora/s3/client/model/S3Object.java | 20 - .../kora/s3/client/model/S3ObjectList.java | 16 - .../kora/s3/client/model/S3ObjectMeta.java | 18 - .../s3/client/model/S3ObjectMetaList.java | 16 - .../kora/s3/client/model/S3ObjectUpload.java | 12 - .../telemetry/DefaultS3ClientLogger.java | 85 -- .../telemetry/DefaultS3ClientTelemetry.java | 58 - .../DefaultS3ClientTelemetryFactory.java | 41 - .../telemetry/DefaultS3KoraClientLogger.java | 75 - .../DefaultS3KoraClientLoggerFactory.java | 17 - .../DefaultS3KoraClientTelemetry.java | 46 - .../DefaultS3KoraClientTelemetryFactory.java | 34 - .../s3/client/telemetry/S3ClientLogger.java | 19 - .../s3/client/telemetry/S3ClientMetrics.java | 14 - .../client/telemetry/S3ClientTelemetry.java | 30 - .../telemetry/S3ClientTelemetryFactory.java | 8 - .../s3/client/telemetry/S3ClientTracer.java | 19 - .../client/telemetry/S3KoraClientLogger.java | 18 - .../telemetry/S3KoraClientLoggerFactory.java | 10 - .../client/telemetry/S3KoraClientMetrics.java | 13 - .../telemetry/S3KoraClientMetricsFactory.java | 10 - .../telemetry/S3KoraClientTelemetry.java | 21 - .../S3KoraClientTelemetryFactory.java | 8 - .../client/telemetry/S3KoraClientTracer.java | 17 - .../telemetry/S3KoraClientTracerFactory.java | 10 - experimental/s3-client-minio/build.gradle | 15 - .../kora/s3/client/minio/MinioS3Body.java | 58 - .../s3/client/minio/MinioS3ClientConfig.java | 35 - .../s3/client/minio/MinioS3ClientModule.java | 108 -- .../MinioS3ClientTelemetryInterceptor.java | 120 -- .../client/minio/MinioS3KoraAsyncClient.java | 368 ----- .../s3/client/minio/MinioS3KoraClient.java | 332 ---- .../kora/s3/client/minio/MinioS3Object.java | 77 - .../s3/client/minio/MinioS3ObjectList.java | 41 - .../s3/client/minio/MinioS3ObjectMeta.java | 66 - .../client/minio/MinioS3ObjectMetaList.java | 16 - .../s3/client/minio/MinioS3ObjectUpload.java | 7 - .../kora/s3/client/minio/package-info.java | 4 - .../s3-client-symbol-processor/build.gradle | 28 +- .../client/symbol/processor/S3ClassNames.kt | 37 + .../processor/S3ClientSymbolProcessor.kt | 1102 +++++--------- .../s3/client/symbol/processor/S3Operation.kt | 14 - .../client/symbol/processor/AbstractS3Test.kt | 33 + .../processor/S3AwsSuspendClientTests.kt | 160 -- ...{S3KoraClientTests.kt => S3ClientTests.kt} | 31 +- .../client/symbol/processor/S3DeleteTest.kt | 60 + .../s3/client/symbol/processor/S3GetTest.kt | 370 +++++ .../processor/S3KoraSuspendClientTests.kt | 608 -------- .../s3/client/symbol/processor/S3ListTest.kt | 114 ++ .../build.gradle | 7 +- .../s3-client/src/main/java/module-info.java | 12 + .../ru/tinkoff/kora/s3/client/S3Client.java | 59 + .../kora/s3/client/S3ClientFactory.java | 5 + .../kora/s3/client/S3ClientModule.java | 37 + .../ru/tinkoff/kora/s3/client/S3Config.java | 65 + .../tinkoff/kora/s3/client/annotation/S3.java | 95 +- .../exception/S3ClientErrorException.java | 34 + .../client/exception/S3ClientException.java | 18 + .../exception/S3ClientResponseException.java | 24 + .../exception/S3ClientUnknownException.java | 18 + .../kora/s3/client/impl/ByteArrayS3Body.java | 60 + .../kora/s3/client/impl/DigestUtils.java | 77 + .../s3/client/impl/InputStreamS3Body.java | 66 + .../impl/KnownSizeAwsChunkedHttpBody.java | 113 ++ .../kora/s3/client/impl/S3ClientImpl.java | 396 +++++ .../kora/s3/client/impl/S3PutHelper.java | 412 +++++ .../kora/s3/client/impl/S3RequestSigner.java | 277 ++++ .../kora/s3/client/impl/UriHelper.java | 43 + .../xml/CompleteMultipartUploadRequest.java | 66 + .../xml/CompleteMultipartUploadResult.java | 33 + ...mpleteMultipartUploadResultSaxHandler.java | 64 + .../client/impl/xml/DeleteObjectsRequest.java | 62 + .../client/impl/xml/DeleteObjectsResult.java | 24 + .../xml/DeleteObjectsResultSaxHandler.java | 131 ++ .../xml/InitiateMultipartUploadResult.java | 25 + ...itiateMultipartUploadResultSaxHandler.java | 58 + .../s3/client/impl/xml/ListBucketResult.java | 23 + .../impl/xml/ListBucketResultSaxHandler.java | 168 +++ .../kora/s3/client/impl/xml/S3Error.java | 29 + .../s3/client/impl/xml/S3ErrorSaxHandler.java | 83 + .../tinkoff/kora/s3/client/model/S3Body.java | 70 + .../kora/s3/client/model/S3Object.java | 17 + .../kora/s3/client/model/S3ObjectMeta.java | 18 + .../s3/client/model/S3ObjectUploadResult.java | 6 + .../tinkoff/kora/s3/client/package-info.java | 0 .../s3/client/telemetry/DefaultS3Logger.java | 47 + .../telemetry/DefaultS3LoggerFactory.java} | 8 +- .../client/telemetry/DefaultS3Telemetry.java | 177 +++ .../telemetry/DefaultS3TelemetryFactory.java | 41 + .../kora/s3/client/telemetry/S3Logger.java | 11 + .../s3/client/telemetry/S3LoggerFactory.java} | 4 +- .../kora/s3/client/telemetry/S3Metrics.java | 8 + .../client/telemetry/S3MetricsFactory.java} | 4 +- .../kora/s3/client/telemetry/S3Telemetry.java | 41 + .../client/telemetry/S3TelemetryFactory.java | 8 + .../kora/s3/client/telemetry/S3Tracer.java | 14 + .../s3/client/telemetry/S3TracerFactory.java} | 4 +- .../tinkoff/kora/s3/client/S3ClientTest.java | 570 +++++++ .../DeleteObjectsResultSaxHandlerTest.java | 41 + .../client/impl/xml/S3ResponseParserTest.java | 25 + .../http/client/async/AsyncHttpClient.java | 3 +- .../src/main/java/module-info.java | 16 + .../kora/http/client/common/HttpClient.java | 14 +- .../telemetry/DefaultHttpClientTelemetry.java | 2 +- .../kora/http/client/jdk/JdkHttpClient.java | 3 +- .../kora/http/client/ok/OkHttpClient.java | 3 +- .../symbol/processor/ClientClassGenerator.kt | 2 +- .../src/main/java/module-info.java | 13 + .../kora/test/kafka/KafkaTestContainer.java | 2 +- .../src/main/java/module-info.java | 9 + .../producer/KafkaPublisherGenerator.kt | 2 +- .../src/main/java/module-info.java | 10 + micrometer/micrometer-module/build.gradle | 2 +- .../kora/micrometer/module/MetricsModule.java | 6 +- .../MicrometerS3ClientMetricsFactory.java | 2 - ...y.java => MicrometerS3MetricsFactory.java} | 10 +- .../Opentelemetry120S3ClientMetrics.java | 2 - ...cs.java => Opentelemetry120S3Metrics.java} | 6 +- .../Opentelemetry123S3ClientMetrics.java | 2 - ...cs.java => Opentelemetry123S3Metrics.java} | 6 +- .../opentelemetry-module/build.gradle | 2 +- .../module/OpentelemetryModule.java | 6 +- .../client/OpentelemetryS3ClientTracer.java | 2 - .../OpentelemetryS3ClientTracerFactory.java | 2 - ...Tracer.java => OpentelemetryS3Tracer.java} | 29 +- ...java => OpentelemetryS3TracerFactory.java} | 8 +- settings.gradle | 4 +- .../kora/ksp/common/AnnotationUtils.kt | 1 + .../tinkoff/kora/ksp/common/CommonAopUtils.kt | 15 +- .../ksp/common/AbstractSymbolProcessorTest.kt | 13 +- .../src/main/java/module-info.java | 5 + 182 files changed, 5934 insertions(+), 9274 deletions(-) create mode 100644 common/src/main/java/module-info.java create mode 100644 config/config-common/src/main/java/module-info.java create mode 100644 experimental/s3-client-annotation-processor/src/main/java/ru/tinkoff/kora/s3/client/annotation/processor/S3ClassNames.java delete mode 100644 experimental/s3-client-annotation-processor/src/main/java/ru/tinkoff/kora/s3/client/annotation/processor/S3Operation.java create mode 100644 experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/AbstractS3Test.java delete mode 100644 experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3AwsAsyncClientTests.java delete mode 100644 experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3AwsClientTests.java delete mode 100644 experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3AwsReactorClientTests.java create mode 100644 experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3DeleteTest.java create mode 100644 experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3GetTest.java delete mode 100644 experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3KoraAsyncClientTests.java delete mode 100644 experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3KoraClientTests.java delete mode 100644 experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3KoraReactorClientTests.java create mode 100644 experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3ListTest.java create mode 100644 experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3PutTest.java delete mode 100644 experimental/s3-client-aws/build.gradle delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3BodyAsync.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3BodySync.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ClientConfig.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ClientModule.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ClientTelemetryInterceptor.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3KoraAsyncClient.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3KoraClient.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3Object.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectList.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectMeta.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectMetaList.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectUpload.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/KoraAwsSdkHttpClient.java delete mode 100644 experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/package-info.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3ClientConfig.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3ClientModule.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3Config.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3DeleteException.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3Exception.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3KoraAsyncClient.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3KoraClient.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3NotFoundException.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/ByteBufferPublisher.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/ByteS3Body.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/InputStreamByteBufferSubscriber.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/InputStreamS3Body.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/PublisherS3Body.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3Body.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3Object.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectList.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectMeta.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectMetaList.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectUpload.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientLogger.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientTelemetry.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientTelemetryFactory.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientLogger.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientLoggerFactory.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientTelemetry.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientTelemetryFactory.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientLogger.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientMetrics.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTelemetry.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTelemetryFactory.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTracer.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientLogger.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientLoggerFactory.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientMetrics.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientMetricsFactory.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTelemetry.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTelemetryFactory.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTracer.java delete mode 100644 experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTracerFactory.java delete mode 100644 experimental/s3-client-minio/build.gradle delete mode 100644 experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3Body.java delete mode 100644 experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ClientConfig.java delete mode 100644 experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ClientModule.java delete mode 100644 experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ClientTelemetryInterceptor.java delete mode 100644 experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3KoraAsyncClient.java delete mode 100644 experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3KoraClient.java delete mode 100644 experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3Object.java delete mode 100644 experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectList.java delete mode 100644 experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectMeta.java delete mode 100644 experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectMetaList.java delete mode 100644 experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectUpload.java delete mode 100644 experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/package-info.java create mode 100644 experimental/s3-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ClassNames.kt create mode 100644 experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/AbstractS3Test.kt delete mode 100644 experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3AwsSuspendClientTests.kt rename experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/{S3KoraClientTests.kt => S3ClientTests.kt} (94%) create mode 100644 experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3DeleteTest.kt create mode 100644 experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3GetTest.kt delete mode 100644 experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3KoraSuspendClientTests.kt create mode 100644 experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ListTest.kt rename experimental/{s3-client-common => s3-client}/build.gradle (50%) create mode 100644 experimental/s3-client/src/main/java/module-info.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3Client.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3ClientFactory.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3ClientModule.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3Config.java rename experimental/{s3-client-common => s3-client}/src/main/java/ru/tinkoff/kora/s3/client/annotation/S3.java (58%) create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientErrorException.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientException.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientResponseException.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientUnknownException.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/ByteArrayS3Body.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/DigestUtils.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/InputStreamS3Body.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/KnownSizeAwsChunkedHttpBody.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3ClientImpl.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3PutHelper.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3RequestSigner.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/UriHelper.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/CompleteMultipartUploadRequest.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/CompleteMultipartUploadResult.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/CompleteMultipartUploadResultSaxHandler.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsRequest.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsResult.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsResultSaxHandler.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/InitiateMultipartUploadResult.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/InitiateMultipartUploadResultSaxHandler.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/ListBucketResult.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/ListBucketResultSaxHandler.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3Error.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3ErrorSaxHandler.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3Body.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3Object.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectMeta.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectUploadResult.java rename experimental/{s3-client-common => s3-client}/src/main/java/ru/tinkoff/kora/s3/client/package-info.java (100%) create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3Logger.java rename experimental/{s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientLoggerFactory.java => s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3LoggerFactory.java} (53%) create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3Telemetry.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3TelemetryFactory.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Logger.java rename experimental/{s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientLoggerFactory.java => s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3LoggerFactory.java} (57%) create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Metrics.java rename experimental/{s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTracerFactory.java => s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3MetricsFactory.java} (56%) create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Telemetry.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3TelemetryFactory.java create mode 100644 experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Tracer.java rename experimental/{s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientMetricsFactory.java => s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3TracerFactory.java} (56%) create mode 100644 experimental/s3-client/src/test/java/ru/tinkoff/kora/s3/client/S3ClientTest.java create mode 100644 experimental/s3-client/src/test/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsResultSaxHandlerTest.java create mode 100644 experimental/s3-client/src/test/java/ru/tinkoff/kora/s3/client/impl/xml/S3ResponseParserTest.java create mode 100644 http/http-client-common/src/main/java/module-info.java create mode 100644 http/http-common/src/main/java/module-info.java create mode 100644 json/json-common/src/main/java/module-info.java create mode 100644 logging/logging-common/src/main/java/module-info.java rename micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/{MicrometerS3KoraClientMetricsFactory.java => MicrometerS3MetricsFactory.java} (59%) rename micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/{Opentelemetry120S3KoraClientMetrics.java => Opentelemetry120S3Metrics.java} (87%) rename micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/{Opentelemetry123S3KoraClientMetrics.java => Opentelemetry123S3Metrics.java} (87%) rename opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/{OpentelemetryS3KoraClientTracer.java => OpentelemetryS3Tracer.java} (61%) rename opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/{OpentelemetryS3KoraClientTracerFactory.java => OpentelemetryS3TracerFactory.java} (59%) create mode 100644 telemetry/telemetry-common/src/main/java/module-info.java diff --git a/annotation-processor-common/src/main/java/ru/tinkoff/kora/annotation/processor/common/CommonClassNames.java b/annotation-processor-common/src/main/java/ru/tinkoff/kora/annotation/processor/common/CommonClassNames.java index f43462089..fb9ec2405 100644 --- a/annotation-processor-common/src/main/java/ru/tinkoff/kora/annotation/processor/common/CommonClassNames.java +++ b/annotation-processor-common/src/main/java/ru/tinkoff/kora/annotation/processor/common/CommonClassNames.java @@ -2,6 +2,7 @@ import com.squareup.javapoet.ClassName; +import java.io.InputStream; import java.util.List; @@ -45,6 +46,7 @@ public class CommonClassNames { public static final ClassName koraGenerated = ClassName.get("ru.tinkoff.kora.common.annotation", "Generated"); public static final ClassName list = ClassName.get(List.class); + public static final ClassName inputStream = ClassName.get(InputStream.class); public static final ClassName config = ClassName.get("ru.tinkoff.kora.config.common", "Config"); public static final ClassName configValueExtractor = ClassName.get("ru.tinkoff.kora.config.common.extractor", "ConfigValueExtractor"); diff --git a/common/src/main/java/module-info.java b/common/src/main/java/module-info.java new file mode 100644 index 000000000..a3488cdf3 --- /dev/null +++ b/common/src/main/java/module-info.java @@ -0,0 +1,16 @@ +module kora.common { + requires transitive jakarta.annotation; + requires transitive kora.application.graph; + + requires static org.reactivestreams; + requires static reactor.core; + requires static kotlinx.coroutines.core; + + exports ru.tinkoff.kora.common; + exports ru.tinkoff.kora.common.annotation; + exports ru.tinkoff.kora.common.readiness; + exports ru.tinkoff.kora.common.naming; + exports ru.tinkoff.kora.common.liveness; + exports ru.tinkoff.kora.common.util; + exports ru.tinkoff.kora.common.util.flow; +} diff --git a/common/src/main/java/ru/tinkoff/kora/common/Tag.java b/common/src/main/java/ru/tinkoff/kora/common/Tag.java index 160e41367..03bcf66bd 100644 --- a/common/src/main/java/ru/tinkoff/kora/common/Tag.java +++ b/common/src/main/java/ru/tinkoff/kora/common/Tag.java @@ -40,6 +40,7 @@ *
* English: A special tag type which means that the tag matches any dependency enforcement condition, i.e. it matches any tag */ + @SuppressWarnings("missing-explicit-ctor") final class Any {} Class[] value(); diff --git a/common/src/main/java/ru/tinkoff/kora/common/annotation/Generated.java b/common/src/main/java/ru/tinkoff/kora/common/annotation/Generated.java index 808d28754..3d0ddb520 100644 --- a/common/src/main/java/ru/tinkoff/kora/common/annotation/Generated.java +++ b/common/src/main/java/ru/tinkoff/kora/common/annotation/Generated.java @@ -3,6 +3,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -12,7 +13,7 @@ * English: Annotation is used to mark source code that has been generated. */ @Retention(RUNTIME) -@Target(TYPE) +@Target({TYPE, METHOD}) public @interface Generated { /** diff --git a/config/config-annotation-processor/src/main/java/ru/tinkoff/kora/config/annotation/processor/ConfigParserGenerator.java b/config/config-annotation-processor/src/main/java/ru/tinkoff/kora/config/annotation/processor/ConfigParserGenerator.java index 990c5b16d..185ca4f9c 100644 --- a/config/config-annotation-processor/src/main/java/ru/tinkoff/kora/config/annotation/processor/ConfigParserGenerator.java +++ b/config/config-annotation-processor/src/main/java/ru/tinkoff/kora/config/annotation/processor/ConfigParserGenerator.java @@ -2,8 +2,6 @@ import com.squareup.javapoet.*; import jakarta.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import ru.tinkoff.kora.annotation.processor.common.*; import ru.tinkoff.kora.common.util.Either; @@ -360,7 +358,8 @@ private TypeSpec buildDefaultsType(DeclaredType type, TypeElement typeElement, L .addOriginatingElement(typeElement) .addAnnotation(AnnotationUtils.generated(ConfigParserGenerator.class)) .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - .addSuperinterface(type); + .addSuperinterface(type) + .addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).build()); for (var tp : typeElement.getTypeParameters()) { defaults.addTypeVariable(TypeVariableName.get(tp)); } diff --git a/config/config-common/src/main/java/module-info.java b/config/config-common/src/main/java/module-info.java new file mode 100644 index 000000000..d8bc7db08 --- /dev/null +++ b/config/config-common/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module kora.config.common { + requires transitive kora.common; + + exports ru.tinkoff.kora.config.common; + exports ru.tinkoff.kora.config.common.annotation; + exports ru.tinkoff.kora.config.common.extractor; + exports ru.tinkoff.kora.config.common.factory; + exports ru.tinkoff.kora.config.common.origin; +} diff --git a/database/database-symbol-processor/src/main/kotlin/ru/tinkoff/kora/database/symbol/processor/DbUtils.kt b/database/database-symbol-processor/src/main/kotlin/ru/tinkoff/kora/database/symbol/processor/DbUtils.kt index c33723cf7..d81498e39 100644 --- a/database/database-symbol-processor/src/main/kotlin/ru/tinkoff/kora/database/symbol/processor/DbUtils.kt +++ b/database/database-symbol-processor/src/main/kotlin/ru/tinkoff/kora/database/symbol/processor/DbUtils.kt @@ -183,7 +183,7 @@ object DbUtils { fun KSFunctionDeclaration.queryMethodBuilder(resolver: Resolver): FunSpec.Builder { - return overridingKeepAop(resolver); + return overridingKeepAop(); } } diff --git a/experimental/s3-client-annotation-processor/build.gradle b/experimental/s3-client-annotation-processor/build.gradle index 166b20218..d563aa807 100644 --- a/experimental/s3-client-annotation-processor/build.gradle +++ b/experimental/s3-client-annotation-processor/build.gradle @@ -5,8 +5,7 @@ dependencies { implementation libs.javapoet - testImplementation project(":experimental:s3-client-aws") - testImplementation project(":experimental:s3-client-minio") + testImplementation project(":experimental:s3-client") testImplementation project(":internal:test-logging") testImplementation project(":config:config-common") testImplementation testFixtures(project(":annotation-processor-common")) diff --git a/experimental/s3-client-annotation-processor/src/main/java/ru/tinkoff/kora/s3/client/annotation/processor/S3ClassNames.java b/experimental/s3-client-annotation-processor/src/main/java/ru/tinkoff/kora/s3/client/annotation/processor/S3ClassNames.java new file mode 100644 index 000000000..a4472a7fd --- /dev/null +++ b/experimental/s3-client-annotation-processor/src/main/java/ru/tinkoff/kora/s3/client/annotation/processor/S3ClassNames.java @@ -0,0 +1,39 @@ +package ru.tinkoff.kora.s3.client.annotation.processor; + +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.TypeName; + +import java.io.InputStream; +import java.util.Set; + +public class S3ClassNames { + + public static class Annotation { + public static final ClassName CLIENT = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "Client"); + public static final ClassName BUCKET = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "Bucket"); + public static final ClassName GET = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "Get"); + public static final ClassName LIST = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "List"); + public static final ClassName LIST_LIMIT = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "List", "Limit"); + public static final ClassName LIST_DELIMITER = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "List", "Delimiter"); + public static final ClassName PUT = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "Put"); + public static final ClassName DELETE = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "Delete"); + + public static final Set OPERATIONS = Set.of(GET, LIST, PUT, DELETE); + } + + + public static final ClassName CLIENT = ClassName.get("ru.tinkoff.kora.s3.client", "S3Client"); + public static final ClassName CLIENT_FACTORY = ClassName.get("ru.tinkoff.kora.s3.client", "S3ClientFactory"); + public static final ClassName S3_BODY = ClassName.get("ru.tinkoff.kora.s3.client.model", "S3Body"); + public static final ClassName S3_OBJECT = ClassName.get("ru.tinkoff.kora.s3.client.model", "S3Object"); + public static final ClassName S3_OBJECT_META = ClassName.get("ru.tinkoff.kora.s3.client.model", "S3ObjectMeta"); + public static final ClassName S3_OBJECT_UPLOAD_RESULT = ClassName.get("ru.tinkoff.kora.s3.client.model", "S3ObjectUploadResult"); + public static final Set BODY_TYPES = Set.of(S3_BODY, ArrayTypeName.of(TypeName.BYTE), ClassName.get(InputStream.class)); + + public static final ClassName RANGE_DATA = ClassName.get("ru.tinkoff.kora.s3.client", "S3Client", "RangeData"); + public static final ClassName RANGE_DATA_RANGE = ClassName.get("ru.tinkoff.kora.s3.client", "S3Client", "RangeData", "Range"); + public static final ClassName RANGE_DATA_START_FROM = ClassName.get("ru.tinkoff.kora.s3.client", "S3Client", "RangeData", "StartFrom"); + public static final ClassName RANGE_DATA_LAST_N = ClassName.get("ru.tinkoff.kora.s3.client", "S3Client", "RangeData", "LastN"); + public static final Set RANGE_CLASSES = Set.of(RANGE_DATA, RANGE_DATA_RANGE, RANGE_DATA_START_FROM, RANGE_DATA_LAST_N); +} diff --git a/experimental/s3-client-annotation-processor/src/main/java/ru/tinkoff/kora/s3/client/annotation/processor/S3ClientAnnotationProcessor.java b/experimental/s3-client-annotation-processor/src/main/java/ru/tinkoff/kora/s3/client/annotation/processor/S3ClientAnnotationProcessor.java index 359a224e6..78040eba2 100644 --- a/experimental/s3-client-annotation-processor/src/main/java/ru/tinkoff/kora/s3/client/annotation/processor/S3ClientAnnotationProcessor.java +++ b/experimental/s3-client-annotation-processor/src/main/java/ru/tinkoff/kora/s3/client/annotation/processor/S3ClientAnnotationProcessor.java @@ -1,82 +1,22 @@ package ru.tinkoff.kora.s3.client.annotation.processor; import com.squareup.javapoet.*; +import jakarta.annotation.Nullable; import ru.tinkoff.kora.annotation.processor.common.*; -import ru.tinkoff.kora.common.Component; -import ru.tinkoff.kora.common.Module; -import ru.tinkoff.kora.s3.client.annotation.processor.S3Operation.ImplType; -import ru.tinkoff.kora.s3.client.annotation.processor.S3Operation.OperationType; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.element.*; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeMirror; import java.io.IOException; -import java.nio.ByteBuffer; +import java.io.InputStream; +import java.io.UncheckedIOException; import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutorService; +import java.util.stream.IntStream; +// todo generate config for bucket annotations public class S3ClientAnnotationProcessor extends AbstractKoraProcessor { - private static final ClassName ANNOTATION_CLIENT = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "Client"); - - private static final ClassName ANNOTATION_OP_GET = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "Get"); - private static final ClassName ANNOTATION_OP_LIST = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "List"); - private static final ClassName ANNOTATION_OP_PUT = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "Put"); - private static final ClassName ANNOTATION_OP_DELETE = ClassName.get("ru.tinkoff.kora.s3.client.annotation", "S3", "Delete"); - - private static final ClassName CLASS_CONFIG = ClassName.get("ru.tinkoff.kora.s3.client", "S3Config"); - private static final ClassName CLASS_AWS_CONFIG = ClassName.get("ru.tinkoff.kora.s3.client.aws", "AwsS3ClientConfig"); - private static final ClassName CLASS_CLIENT_CONFIG = ClassName.get("ru.tinkoff.kora.s3.client", "S3ClientConfig"); - private static final ClassName CLASS_CLIENT_SIMPLE_SYNC = ClassName.get("ru.tinkoff.kora.s3.client", "S3KoraClient"); - private static final ClassName CLASS_CLIENT_SIMPLE_ASYNC = ClassName.get("ru.tinkoff.kora.s3.client", "S3KoraAsyncClient"); - private static final ClassName CLASS_CLIENT_AWS_SYNC = ClassName.get("software.amazon.awssdk.services.s3", "S3Client"); - private static final ClassName CLASS_CLIENT_AWS_ASYNC = ClassName.get("software.amazon.awssdk.services.s3", "S3AsyncClient"); - private static final ClassName CLASS_CLIENT_AWS_ASYNC_MULTIPART = ClassName.get("software.amazon.awssdk.services.s3.internal.multipart", "MultipartS3AsyncClient"); - private static final ClassName CLASS_INTERCEPTOR_AWS_CONTEXT_KEY = ClassName.get("ru.tinkoff.kora.s3.client.aws", "AwsS3ClientTelemetryInterceptor"); - private static final ClassName CLASS_INTERCEPTOR_AWS_OPERATION = ClassName.get("ru.tinkoff.kora.s3.client.aws", "AwsS3ClientTelemetryInterceptor", "Operation"); - private static final ClassName CLASS_CLIENT_AWS_TAG = ClassName.get("software.amazon.awssdk.awscore", "AwsClient"); - private static final ClassName CLASS_CLIENT_AWS_MULTIPART_TAG = ClassName.get("software.amazon.awssdk.services.s3.model", "MultipartUpload"); - - private static final ClassName CLASS_S3_TELEMETRY = ClassName.get("ru.tinkoff.kora.s3.client.telemetry", "S3ClientTelemetry"); - private static final ClassName CLASS_S3_TELEMETRY_FACTORY = ClassName.get("ru.tinkoff.kora.s3.client.telemetry", "S3ClientTelemetryFactory"); - private static final ClassName CLASS_S3_EXCEPTION = ClassName.get("ru.tinkoff.kora.s3.client", "S3Exception"); - private static final ClassName CLASS_S3_EXCEPTION_NOT_FOUND = ClassName.get("ru.tinkoff.kora.s3.client", "S3NotFoundException"); - private static final ClassName CLASS_S3_BODY = ClassName.get("ru.tinkoff.kora.s3.client.model", "S3Body"); - private static final ClassName CLASS_S3_UPLOAD = ClassName.get("ru.tinkoff.kora.s3.client.model", "S3ObjectUpload"); - private static final ClassName CLASS_S3_BODY_BYTES = ClassName.get("ru.tinkoff.kora.s3.client.model", "ByteS3Body"); - private static final ClassName CLASS_S3_BODY_PUBLISHER = ClassName.get("ru.tinkoff.kora.s3.client.model", "PublisherS3Body"); - private static final ClassName CLASS_S3_OBJECT = ClassName.get("ru.tinkoff.kora.s3.client.model", "S3Object"); - private static final ClassName CLASS_S3_OBJECT_META = ClassName.get("ru.tinkoff.kora.s3.client.model", "S3ObjectMeta"); - private static final TypeName CLASS_S3_OBJECT_MANY = ParameterizedTypeName.get(ClassName.get(List.class), CLASS_S3_OBJECT); - private static final TypeName CLASS_S3_OBJECT_META_MANY = ParameterizedTypeName.get(ClassName.get(List.class), CLASS_S3_OBJECT_META); - private static final ClassName CLASS_S3_OBJECT_LIST = ClassName.get("ru.tinkoff.kora.s3.client.model", "S3ObjectList"); - private static final ClassName CLASS_S3_OBJECT_META_LIST = ClassName.get("ru.tinkoff.kora.s3.client.model", "S3ObjectMetaList"); - - private static final ClassName CLASS_JDK_FLOW_ADAPTER = ClassName.get("reactor.adapter", "JdkFlowAdapter"); - - private static final ClassName CLASS_AWS_EXCEPTION_NO_KEY = ClassName.get("software.amazon.awssdk.services.s3.model", "NoSuchKeyException"); - private static final ClassName CLASS_AWS_EXCEPTION_NO_BUCKET = ClassName.get("software.amazon.awssdk.services.s3.model", "NoSuchBucketException"); - private static final ClassName CLASS_AWS_EXCEPTION = ClassName.get("software.amazon.awssdk.awscore.exception", "AwsServiceException"); - private static final ClassName CLASS_AWS_IS_SYNC_BODY = ClassName.get("software.amazon.awssdk.core.sync", "RequestBody"); - private static final ClassName CLASS_AWS_IS_ASYNC_BODY = ClassName.get("software.amazon.awssdk.core.async", "AsyncRequestBody"); - private static final ClassName CLASS_AWS_IS_ASYNC_TRANSFORMER = ClassName.get("software.amazon.awssdk.core.async", "AsyncResponseTransformer"); - private static final ClassName CLASS_AWS_GET_REQUEST = ClassName.get("software.amazon.awssdk.services.s3.model", "GetObjectRequest"); - private static final ClassName CLASS_AWS_GET_RESPONSE = ClassName.get("software.amazon.awssdk.services.s3.model", "GetObjectResponse"); - private static final TypeName CLASS_AWS_GET_IS_RESPONSE = ParameterizedTypeName.get(ClassName.get("software.amazon.awssdk.core", "ResponseInputStream"), CLASS_AWS_GET_RESPONSE); - private static final ClassName CLASS_AWS_DELETE_REQUEST = ClassName.get("software.amazon.awssdk.services.s3.model", "DeleteObjectRequest"); - private static final ClassName CLASS_AWS_DELETE_RESPONSE = ClassName.get("software.amazon.awssdk.services.s3.model", "DeleteObjectResponse"); - private static final ClassName CLASS_AWS_DELETES_REQUEST = ClassName.get("software.amazon.awssdk.services.s3.model", "DeleteObjectsRequest"); - private static final ClassName CLASS_AWS_DELETES_RESPONSE = ClassName.get("software.amazon.awssdk.services.s3.model", "DeleteObjectsResponse"); - private static final ClassName CLASS_AWS_LIST_REQUEST = ClassName.get("software.amazon.awssdk.services.s3.model", "ListObjectsV2Request"); - private static final ClassName CLASS_AWS_LIST_RESPONSE = ClassName.get("software.amazon.awssdk.services.s3.model", "ListObjectsV2Response"); - private static final ClassName CLASS_AWS_PUT_REQUEST = ClassName.get("software.amazon.awssdk.services.s3.model", "PutObjectRequest"); - private static final ClassName CLASS_AWS_PUT_RESPONSE = ClassName.get("software.amazon.awssdk.services.s3.model", "PutObjectResponse"); - @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); @@ -84,12 +24,12 @@ public synchronized void init(ProcessingEnvironment processingEnv) { @Override public Set getSupportedAnnotationTypes() { - return Set.of(ANNOTATION_CLIENT.canonicalName()); + return Set.of(S3ClassNames.Annotation.CLIENT.canonicalName()); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { - var annotation = processingEnv.getElementUtils().getTypeElement(ANNOTATION_CLIENT.canonicalName()); + var annotation = processingEnv.getElementUtils().getTypeElement(S3ClassNames.Annotation.CLIENT.canonicalName()); if (annotation == null) { return false; } @@ -103,13 +43,15 @@ public boolean process(Set annotations, RoundEnvironment var packageName = getPackage(s3client); try { - TypeSpec spec = generateClient(s3client); + var configSpec = generateClientConfig(s3client); + if (!configSpec.paths.isEmpty()) { + var configFile = JavaFile.builder(packageName, configSpec.type).build(); + configFile.writeTo(processingEnv.getFiler()); + } + + var spec = generateClient(s3client, configSpec); var implFile = JavaFile.builder(packageName, spec).build(); implFile.writeTo(processingEnv.getFiler()); - - TypeSpec configSpec = generateClientConfig(s3client); - var configFile = JavaFile.builder(packageName, configSpec).build(); - configFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { throw new IllegalStateException(e); } @@ -118,71 +60,46 @@ public boolean process(Set annotations, RoundEnvironment return false; } - private TypeSpec generateClient(TypeElement s3client) { - var implSpecBuilder = TypeSpec.classBuilder(NameUtils.generatedType(s3client, "Impl")) + private TypeSpec generateClient(TypeElement s3client, GenerateConfigResult configSpec) { + var implClassName = ClassName.get(getPackage(s3client), NameUtils.generatedType(s3client, "Impl")); + var implSpecBuilder = TypeSpec.classBuilder(implClassName) .addModifiers(Modifier.FINAL, Modifier.PUBLIC) .addAnnotation(AnnotationUtils.generated(S3ClientAnnotationProcessor.class)) - .addAnnotation(Component.class) - .addSuperinterface(s3client.asType()); + .addSuperinterface(s3client.asType()) + .addOriginatingElement(s3client); + + var constructorBuilder = MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC); + var clientAnnotation = AnnotationUtils.findAnnotation(s3client, S3ClassNames.Annotation.CLIENT); + var clientTag = AnnotationUtils.>parseAnnotationValueWithoutDefault(clientAnnotation, "clientFactoryTag"); + if (clientTag != null) { + constructorBuilder.addParameter(ParameterSpec.builder(S3ClassNames.CLIENT_FACTORY, "clientFactory") + .addAnnotation(AnnotationSpec.builder(CommonClassNames.tag) + .addMember("value", TagUtils.writeTagAnnotationValue(clientTag)) + .build() + ) + .build()); + } else { + constructorBuilder.addParameter(S3ClassNames.CLIENT_FACTORY, "clientFactory"); + } + if (!configSpec.paths.isEmpty()) { + var configTypeName = ClassName.get(implClassName.packageName(), configSpec.type.name); + constructorBuilder.addParameter(configTypeName, "config") + .addStatement("this.config = config"); + implSpecBuilder.addField(configTypeName, "config", Modifier.PRIVATE, Modifier.FINAL); + } - final Set constructed = new HashSet<>(); - var constructorBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); - var constructorCode = CodeBlock.builder(); - implSpecBuilder.addField(CLASS_CLIENT_CONFIG, "_clientConfig", Modifier.PRIVATE, Modifier.FINAL); - constructorCode.addStatement("this._clientConfig = clientConfig"); - constructorBuilder.addParameter(ParameterSpec.builder(CLASS_CLIENT_CONFIG, "clientConfig") - .addAnnotation(TagUtils.makeAnnotationSpecForTypes(TypeName.get(s3client.asType()))) - .build()); + var constructorCode = CodeBlock.builder() + .addStatement("this.client = clientFactory.create($T.class)", implClassName); + implSpecBuilder.addField(S3ClassNames.CLIENT, "client", Modifier.PRIVATE, Modifier.FINAL); - for (Element enclosedElement : s3client.getEnclosedElements()) { + for (var enclosedElement : s3client.getEnclosedElements()) { if (enclosedElement instanceof ExecutableElement method) { - var operationType = getOperationType(method); - if (operationType.isEmpty() && !method.getModifiers().contains(Modifier.DEFAULT)) { - throw new ProcessingErrorException("@S3.Client method without operation annotation can't be non default", method); - } else if (operationType.isPresent()) { - S3Operation operation = getOperation(method, operationType.get()); - MethodSpec methodSpec = MethodSpec.overriding(method) - .addCode(operation.code()) - .build(); - - implSpecBuilder.addMethod(methodSpec); - - final List signatures = new ArrayList<>(); - if (operation.impl() == ImplType.SIMPLE) { - if (operation.mode() == S3Operation.Mode.SYNC) { - signatures.add(new Signature(CLASS_CLIENT_SIMPLE_SYNC, "simpleSyncClient")); - } else { - signatures.add(new Signature(CLASS_CLIENT_SIMPLE_ASYNC, "simpleAsyncClient")); - } - } else if (operation.impl() == ImplType.AWS) { - if (operation.mode() == S3Operation.Mode.SYNC) { - signatures.add(new Signature(CLASS_CLIENT_AWS_SYNC, "awsSyncClient")); - } else { - signatures.add(new Signature(CLASS_CLIENT_AWS_ASYNC, "awsAsyncClient")); - } - if (operation.type() == OperationType.PUT) { - signatures.add(new Signature(CLASS_CLIENT_AWS_ASYNC_MULTIPART, "awsAsyncMultipartClient", List.of(CLASS_CLIENT_AWS_MULTIPART_TAG))); - signatures.add(new Signature(CLASS_CLIENT_AWS_ASYNC, "awsAsyncClient")); - signatures.add(new Signature(CLASS_AWS_CONFIG, "awsClientConfig")); - signatures.add(new Signature(ClassName.get(ExecutorService.class), "awsAsyncExecutor", List.of(CLASS_CLIENT_AWS_TAG))); - } - } - - for (Signature signature : signatures) { - if (!constructed.contains(signature)) { - if (signature.tags().isEmpty()) { - constructorBuilder.addParameter(signature.type(), signature.name()); - } else { - constructorBuilder.addParameter(ParameterSpec.builder(signature.type(), signature.name()) - .addAnnotation(TagUtils.makeAnnotationSpecForTypes(signature.tags())) - .build()); - } - implSpecBuilder.addField(signature.type(), "_" + signature.name, Modifier.PRIVATE, Modifier.FINAL); - constructorCode.addStatement("this._" + signature.name() + " = " + signature.name()); - constructed.add(signature); - } - } + if (method.getModifiers().contains(Modifier.DEFAULT) || method.getModifiers().contains(Modifier.STATIC)) { + continue; } + var operation = generateOperation(configSpec, method); + implSpecBuilder.addMethod(operation); } } @@ -192,847 +109,464 @@ private TypeSpec generateClient(TypeElement s3client) { return implSpecBuilder.build(); } - private TypeSpec generateClientConfig(TypeElement s3client) { - var clientAnnotation = AnnotationUtils.findAnnotation(s3client, ANNOTATION_CLIENT); - final String clientConfigPath = AnnotationUtils.parseAnnotationValueWithoutDefault(clientAnnotation, "value"); + record GenerateConfigResult(@Nullable TypeSpec type, List paths) {} - var extractorClass = ParameterizedTypeName.get(CommonClassNames.configValueExtractor, CLASS_CLIENT_CONFIG); - return TypeSpec.interfaceBuilder(NameUtils.generatedType(s3client, "ClientConfigModule")) - .addModifiers(Modifier.PUBLIC) - .addAnnotation(AnnotationUtils.generated(S3ClientAnnotationProcessor.class)) - .addAnnotation(AnnotationSpec.builder(Module.class).build()) - .addOriginatingElement(s3client) - .addMethod(MethodSpec.methodBuilder("clientConfig") - .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT) - .addAnnotation(TagUtils.makeAnnotationSpecForTypes(TypeName.get(s3client.asType()))) - .addParameter(ParameterSpec.builder(CommonClassNames.config, "config").build()) - .addParameter(ParameterSpec.builder(extractorClass, "extractor").build()) - .addStatement("var value = config.get($S)", clientConfigPath) - .addStatement("return $T.ofNullable(extractor.extract(value)).orElseThrow(() -> $T.missingValueAfterParse(value))", Optional.class, CommonClassNames.configValueExtractionException) - .returns(CLASS_CLIENT_CONFIG) - .build()) - .build(); - } - - record Signature(TypeName type, String name, List tags) { - - public Signature(TypeName type, String name) { - this(type, name, Collections.emptyList()); + private GenerateConfigResult generateClientConfig(TypeElement s3client) { + var bucketPaths = new LinkedHashSet(); + var onClass = AnnotationUtils.findAnnotation(s3client, S3ClassNames.Annotation.BUCKET); + if (onClass != null) { + var value = AnnotationUtils.parseAnnotationValueWithoutDefault(onClass, "value"); + if (value == null) { + throw new ProcessingErrorException("@S3.Bucket annotation is missing value", s3client, onClass); + } + bucketPaths.add(value); } - } - - record OperationMeta(OperationType type, AnnotationMirror annotation) {} - - private Optional getOperationType(ExecutableElement method) { - Optional value = Optional.empty(); - - for (AnnotationMirror annotationMirror : method.getAnnotationMirrors()) { - OperationType type = null; - if (ClassName.get(annotationMirror.getAnnotationType()).equals(ANNOTATION_OP_GET)) { - type = OperationType.GET; - } else if (ClassName.get(annotationMirror.getAnnotationType()).equals(ANNOTATION_OP_LIST)) { - type = OperationType.LIST; - } else if (ClassName.get(annotationMirror.getAnnotationType()).equals(ANNOTATION_OP_PUT)) { - type = OperationType.PUT; - } else if (ClassName.get(annotationMirror.getAnnotationType()).equals(ANNOTATION_OP_DELETE)) { - type = OperationType.DELETE; + for (var enclosedElement : s3client.getEnclosedElements()) { + if (enclosedElement.getKind() != ElementKind.METHOD) { + continue; } - - if (value.isEmpty() && type != null) { - value = Optional.of(new OperationMeta(type, annotationMirror)); - } else { - throw new ProcessingErrorException("@S3.Client method must be annotated with single operation annotation", method); + if (enclosedElement.getModifiers().contains(Modifier.DEFAULT) || enclosedElement.getModifiers().contains(Modifier.STATIC)) { + continue; + } + var onMethod = AnnotationUtils.findAnnotation(enclosedElement, S3ClassNames.Annotation.BUCKET); + if (onMethod == null) { + continue; + } + var value = AnnotationUtils.parseAnnotationValueWithoutDefault(onMethod, "value"); + if (value == null) { + throw new ProcessingErrorException("@S3.Bucket annotation is missing value", enclosedElement, onMethod); } + bucketPaths.add(value); + } + var paths = new ArrayList<>(bucketPaths); + if (bucketPaths.isEmpty()) { + return new GenerateConfigResult(null, paths); + } + var configType = ClassName.get(getPackage(s3client), NameUtils.generatedType(s3client, "ClientConfig")); + var b = TypeSpec.classBuilder(configType) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addAnnotation(AnnotationUtils.generated(S3ClientAnnotationProcessor.class)) + .addOriginatingElement(s3client); + var constructor = MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(CommonClassNames.config, "config"); + var equals = MethodSpec.methodBuilder("equals") + .addModifiers(Modifier.PUBLIC) + .addParameter(ClassName.get(Object.class), "o") + .returns(TypeName.BOOLEAN) + .beginControlFlow("if (o instanceof $T that)", configType) + .addStatement("return $L", IntStream.range(0, paths.size()) + .mapToObj(i -> CodeBlock.of("$T.equals(this.bucket_$L, that.bucket_$L)", Objects.class, i, i)) + .collect(CodeBlock.joining("\n && "))) + .nextControlFlow("else") + .addStatement("return false") + .endControlFlow() + .build(); + var hashCode = MethodSpec.methodBuilder("hashCode") + .addModifiers(Modifier.PUBLIC) + .returns(TypeName.INT) + .addStatement("return $T.hash($L)", Objects.class, IntStream.range(0, paths.size()).mapToObj(i -> CodeBlock.of("bucket_$L", i)).collect(CodeBlock.joining(", "))) + .build(); + for (var i = 0; i < paths.size(); i++) { + var path = paths.get(i); + b.addField(String.class, "bucket_" + i, Modifier.PUBLIC, Modifier.FINAL); + constructor.addStatement("this.bucket_$L = config.get($S).asString()", i, path); } - return value; + b.addMethod(constructor.build()); + b.addMethod(equals); + b.addMethod(hashCode); + return new GenerateConfigResult(b.build(), paths); } - private S3Operation getOperation(ExecutableElement method, OperationMeta operationMeta) { - if (MethodUtils.isPublisher(method)) { - throw new ProcessingErrorException("@S3.%s operation method return signature can't be: Publisher".formatted( - operationMeta.type().name()), method); - } else if (MethodUtils.isFlux(method)) { - throw new ProcessingErrorException("@S3.%s operation method return signature can't be: Flux".formatted( - operationMeta.type().name()), method); + private MethodSpec generateOperation(GenerateConfigResult configSpec, ExecutableElement method) { + var operationAnnotations = method.getAnnotationMirrors() + .stream() + .filter(a -> S3ClassNames.Annotation.OPERATIONS.contains(ClassName.get((TypeElement) a.getAnnotationType().asElement()))) + .toList(); + if (operationAnnotations.size() != 1) { + throw new ProcessingErrorException("Method " + method.getSimpleName() + " has " + operationAnnotations.size() + " S3 annotations, but should have exactly one", method); } - - final S3Operation.Mode mode = MethodUtils.isFuture(method) || MethodUtils.isMono(method) - ? S3Operation.Mode.ASYNC - : S3Operation.Mode.SYNC; - - if (OperationType.GET == operationMeta.type) { - return operationGET(method, operationMeta, mode); - } else if (OperationType.LIST == operationMeta.type) { - return operationLIST(method, operationMeta, mode); - } else if (OperationType.PUT == operationMeta.type) { - return operationPUT(method, operationMeta, mode); - } else if (OperationType.DELETE == operationMeta.type) { - return operationDELETE(method, operationMeta, mode); + var operationAnnotation = operationAnnotations.get(0); + var operationAnnotationClassName = ClassName.get((TypeElement) operationAnnotation.getAnnotationType().asElement()); + + if (operationAnnotationClassName.equals(S3ClassNames.Annotation.GET)) { + return operationGET(configSpec, method, operationAnnotation); + } else if (operationAnnotationClassName.equals(S3ClassNames.Annotation.LIST)) { + return operationLIST(configSpec, method, operationAnnotation); + } else if (operationAnnotationClassName.equals(S3ClassNames.Annotation.PUT)) { + return operationPUT(configSpec, method, operationAnnotation); + } else if (operationAnnotationClassName.equals(S3ClassNames.Annotation.DELETE)) { + return operationDELETE(configSpec, method, operationAnnotation); } else { - throw new UnsupportedOperationException("Unsupported S3 operation type"); + throw new IllegalStateException("Unsupported S3 operation type: " + operationAnnotationClassName); } } - private S3Operation operationGET(ExecutableElement method, OperationMeta operationMeta, S3Operation.Mode mode) { - final String keyMapping = AnnotationUtils.parseAnnotationValueWithoutDefault(operationMeta.annotation, "value"); - final Key key; - final VariableElement firstParameter = method.getParameters().stream().findFirst().orElse(null); - if (keyMapping != null && !keyMapping.isBlank()) { - key = parseKey(method, keyMapping); - if (key.params().isEmpty() && !method.getParameters().isEmpty()) { - throw new ProcessingErrorException("@S3.Get operation key template must use method arguments or they should be removed", method); - } - } else if (method.getParameters().size() > 1) { - throw new ProcessingErrorException("@S3.Get operation can't have multiple method parameters for keys without key template", method); - } else if (method.getParameters().isEmpty()) { - throw new ProcessingErrorException("@S3.Get operation must have key parameter", method); - } else { - key = new Key(CodeBlock.of("var _key = String.valueOf($L)", firstParameter.toString()), List.of(firstParameter)); + private MethodSpec operationGET(GenerateConfigResult configSpec, ExecutableElement method, AnnotationMirror operationAnnotation) { + var returnType = TypeName.get(method.getReturnType()); + var byteArrayTypeName = ArrayTypeName.of(TypeName.BYTE); + var allowedTypeNames = Set.of( + S3ClassNames.S3_OBJECT_META, + ParameterizedTypeName.get(ClassName.get(Optional.class), S3ClassNames.S3_OBJECT_META), + byteArrayTypeName, + ParameterizedTypeName.get(ClassName.get(Optional.class), byteArrayTypeName), + S3ClassNames.S3_BODY, + S3ClassNames.S3_OBJECT, + CommonClassNames.inputStream + ); + if (!allowedTypeNames.contains(returnType)) { + throw new ProcessingErrorException("Method " + method.getSimpleName() + " has return type " + returnType + ", but should have one of " + allowedTypeNames, method); } - - boolean isOptional = MethodUtils.isOptional(method); - boolean isMono = MethodUtils.isMono(method); - final TypeName returnType; - if (mode == S3Operation.Mode.SYNC) { - if (isOptional) { - returnType = ClassName.get(MethodUtils.getGenericType(method.getReturnType()).orElseThrow()); + var rangeParams = method.getParameters() + .stream() + .filter(p -> S3ClassNames.RANGE_CLASSES.contains(TypeName.get(p.asType()))) + .toList(); + if (rangeParams.size() > 1) { + throw new ProcessingErrorException("Method " + method.getSimpleName() + " has more than one range parameter", rangeParams.get(1)); + } + var range = rangeParams.isEmpty() ? null : rangeParams.get(0); + var bucket = extractBucket(configSpec, method); + var key = extractKey(method, operationAnnotation, true); + var methodBuilder = CommonUtils.overridingKeepAop(method) + .addStatement("var _bucket = $L", bucket) + .addStatement("var _key = $L", key); + var isNullable = CommonUtils.isNullable(method); + if (returnType.equals(S3ClassNames.S3_OBJECT_META)) { + if (range != null) { + throw new ProcessingErrorException("Range parameters are not allowed on metadata requests", range); + } + if (isNullable) { + methodBuilder.addStatement("return this.client.getMetaOptional(_bucket, _key)"); } else { - returnType = ClassName.get(method.getReturnType()); + methodBuilder.addStatement("return $T.requireNonNull(this.client.getMeta(_bucket, _key))", Objects.class); } - } else { - returnType = ClassName.get(MethodUtils.getGenericType(method.getReturnType()).orElseThrow()); + return methodBuilder.build(); } - - var codeBuilder = CodeBlock.builder(); - for (VariableElement parameter : method.getParameters()) { - if(!(method.getReturnType() instanceof PrimitiveType)) { - codeBuilder.add(""" - if($T.isNull($L)) { - throw new $T("S3.Get request key argument expected, but was null for arg: $L"); - } - """, Objects.class, parameter.getSimpleName().toString(), IllegalArgumentException.class, parameter.getSimpleName().toString()); + if (returnType.equals(ParameterizedTypeName.get(ClassName.get(Optional.class), S3ClassNames.S3_OBJECT_META))) { + if (range != null) { + throw new ProcessingErrorException("Range parameters are not allowed on metadata requests", range); } + methodBuilder.addStatement("var _meta = this.client.getMetaOptional(_bucket, _key)"); + return methodBuilder.addStatement("return $T.ofNullable(_meta)", Optional.class).build(); } - codeBuilder.add("\n"); - - if (CLASS_S3_OBJECT.equals(returnType) || CLASS_S3_OBJECT_META.equals(returnType)) { - if (firstParameter != null && CommonUtils.isCollection(firstParameter.asType())) { - throw new ProcessingErrorException("@S3.Get operation expected single result, but parameter is collection of keys", method); - } - - var bodyBuilder = CodeBlock.builder(); - if (mode == S3Operation.Mode.SYNC) { - bodyBuilder.add("_simpleSyncClient"); + if (range != null) { + methodBuilder.addStatement("var _range = $L", range); + } else { + methodBuilder.addStatement("var _range = ($T) null", S3ClassNames.RANGE_DATA); + } + if (returnType.equals(byteArrayTypeName) || returnType.equals(ParameterizedTypeName.get(ClassName.get(Optional.class), byteArrayTypeName))) { + var isOptional = CommonUtils.isOptional(method.getReturnType()); + if (isNullable || isOptional) { + methodBuilder.beginControlFlow("try (var _object = this.client.getOptional(_bucket, _key, _range))") + .beginControlFlow("if (_object == null)") + .addStatement("return $L", isOptional ? CodeBlock.of("$T.empty()", Optional.class) : CodeBlock.of("null")) + .endControlFlow() + .beginControlFlow("try (var _body = _object.body())") + .addStatement("var _bytes = _body.asBytes()") + .addStatement("return $L", isOptional ? CodeBlock.of("$T.of(_bytes)", Optional.class) : CodeBlock.of("_bytes")) + .endControlFlow() + .nextControlFlow("catch ($T _e)", IOException.class) + .addStatement("throw new $T(_e)", UncheckedIOException.class) + .endControlFlow(); } else { - bodyBuilder.add("_simpleAsyncClient"); + methodBuilder.beginControlFlow("try (var _object = this.client.get(_bucket, _key, _range); var _body = _object.body())") + .addStatement("return $T.requireNonNull(_body.asBytes())", Objects.class) + .nextControlFlow("catch ($T _e)", IOException.class) + .addStatement("throw new $T(_e)", UncheckedIOException.class) + .endControlFlow(); } - - if (CLASS_S3_OBJECT.equals(returnType)) { - bodyBuilder.add(".get(_clientConfig.bucket(), _key)"); + return methodBuilder.build(); + } + if (returnType.equals(S3ClassNames.S3_OBJECT)) { + if (isNullable) { + methodBuilder.addStatement("return this.client.getOptional(_bucket, _key, _range)"); } else { - bodyBuilder.add(".getMeta(_clientConfig.bucket(), _key)"); - } - - if (mode == S3Operation.Mode.ASYNC && CompletableFuture.class.getCanonicalName().equals(((DeclaredType) method.getReturnType()).asElement().toString())) { - bodyBuilder.add(".toCompletableFuture()"); + methodBuilder.addStatement("return this.client.get(_bucket, _key, _range)"); } - - codeBuilder.addStatement(key.code()); - if (isOptional) { - codeBuilder - .beginControlFlow("try") - .add("return $T.of(", Optional.class) - .add(bodyBuilder.build()) - .add(");\n") - .nextControlFlow("catch($T e)", CLASS_S3_EXCEPTION_NOT_FOUND) - .addStatement("return $T.empty()", Optional.class) + return methodBuilder.build(); + } + if (returnType.equals(S3ClassNames.S3_BODY)) { + if (isNullable) { + methodBuilder.addStatement("var _object = this.client.getOptional(_bucket, _key, _range)") + .beginControlFlow("if (_object == null)") + .addStatement("return null") + .nextControlFlow("else") + .addStatement("return _object.body()") .endControlFlow(); - } else if (isMono) { - codeBuilder - .beginControlFlow("return $T.fromCompletionStage(() -> ", CommonClassNames.mono) - .add("return ").add(bodyBuilder.build()).add("\n") - .add(""" - .exceptionallyCompose(e -> { - Throwable cause = e; - if (e instanceof $T) { - cause = e.getCause(); - } - if(cause instanceof $T) { - return $T.completedFuture(null); - } else { - return $T.failedFuture(cause); - } - }); - """, CompletionException.class, CLASS_S3_EXCEPTION_NOT_FOUND, CompletableFuture.class, CompletableFuture.class) - .endControlFlow(")"); } else { - codeBuilder.add("return ").add(bodyBuilder.build()).add(";\n"); - } - - return new S3Operation(method, operationMeta.annotation, OperationType.GET, ImplType.SIMPLE, mode, codeBuilder.build()); - } else if (CLASS_S3_OBJECT_MANY.equals(returnType) || CLASS_S3_OBJECT_META_MANY.equals(returnType)) { - if (firstParameter != null && !CommonUtils.isCollection(firstParameter.asType())) { - throw new ProcessingErrorException("@S3.Get operation expected many results, but parameter isn't collection of keys", method); - } else if (keyMapping != null && !keyMapping.isBlank()) { - throw new ProcessingErrorException("@S3.Get operation expected many results, key template can't be specified for collection of keys", method); - } else if (isOptional) { - throw new ProcessingErrorException("@S3.Get operation with multiple keys, can't be return type Optional", method); - } - - var bodyBuilder = CodeBlock.builder(); - - String clientField = mode == S3Operation.Mode.SYNC - ? "_simpleSyncClient" - : "_simpleAsyncClient"; - - if (isMono) { - bodyBuilder.beginControlFlow("return $T.fromCompletionStage(() -> ", CommonClassNames.mono); + methodBuilder.addStatement("return this.client.get(_bucket, _key, _range).body()"); } - - if (CLASS_S3_OBJECT_MANY.equals(returnType)) { - bodyBuilder.add("return $L.get(_clientConfig.bucket(), $L)", - clientField, firstParameter.getSimpleName().toString()); + return methodBuilder.build(); + } + if (returnType.equals(CommonClassNames.inputStream)) { + if (isNullable) { + methodBuilder.addStatement("var _object = this.client.getOptional(_bucket, _key, _range)") + .beginControlFlow("if (_object == null)") + .addStatement("return null") + .nextControlFlow("else") + .addStatement("return _object.body().asInputStream()") + .endControlFlow(); } else { - bodyBuilder.add("return $L.getMeta(_clientConfig.bucket(), $L)", - clientField, firstParameter.getSimpleName().toString()); + methodBuilder.addStatement("return this.client.get(_bucket, _key, _range).body().asInputStream()"); } + return methodBuilder.build(); + } + throw new IllegalStateException("Not gonna happen"); + } - if (mode == S3Operation.Mode.ASYNC && CompletableFuture.class.getCanonicalName().equals(((DeclaredType) method.getReturnType()).asElement().toString())) { - bodyBuilder.add(".toCompletableFuture()"); - } - bodyBuilder.add(";\n"); + private MethodSpec operationLIST(GenerateConfigResult configSpec, ExecutableElement method, AnnotationMirror operationAnnotation) { + var returnType = TypeName.get(method.getReturnType()); + var isList = returnType.equals(ParameterizedTypeName.get(ClassName.get(List.class), S3ClassNames.S3_OBJECT_META)); + var isIterator = returnType.equals(ParameterizedTypeName.get(ClassName.get(Iterator.class), S3ClassNames.S3_OBJECT_META)); + if (!isList && !isIterator) { + throw new ProcessingErrorException("Method " + method.getSimpleName() + " has return type " + returnType + ", but should have one of Iterator or List", method); + } + var bucket = extractBucket(configSpec, method); + var key = extractKey(method, operationAnnotation, false); + var limit = extractLimit(method); + var delimiter = extractDelimiter(method); + + var methodBuilder = CommonUtils.overridingKeepAop(method) + .addStatement("var _bucket = $L", bucket) + .addStatement("var _key = $L", key) + .addStatement("var _delimiter = $L", delimiter) + .addStatement("var _limit = $L", limit); + + if (isList) { + return methodBuilder + .addStatement("return this.client.list(_bucket, _key, _delimiter, _limit)") + .build(); + } + if (isIterator) { + return methodBuilder + .addStatement("return this.client.listIterator(_bucket, _key, _delimiter, _limit)") + .build(); + } + throw new IllegalStateException("never gonna happen"); + } - if (isMono) { - bodyBuilder.endControlFlow(")"); + private CodeBlock extractLimit(ExecutableElement method) { + var onParameter = method.getParameters() + .stream() + .filter(p -> AnnotationUtils.isAnnotationPresent(p, S3ClassNames.Annotation.LIST_LIMIT)) + .toList(); + if (onParameter.size() > 1) { + throw new ProcessingErrorException("@S3.List operation expected single @S3.List.Limit parameter", method); + } + if (!onParameter.isEmpty()) { + var parameter = onParameter.get(0); + var annotation = AnnotationUtils.findAnnotation(parameter, S3ClassNames.Annotation.LIST_LIMIT); + if (AnnotationUtils.parseAnnotationValueWithoutDefault(annotation, "value") != null) { + throw new ProcessingErrorException("@S3.List.Limit annotation can't have value when annotating parameter", parameter, annotation); } - - return new S3Operation(method, operationMeta.annotation, OperationType.GET, ImplType.SIMPLE, mode, bodyBuilder.build()); - } else if (CLASS_AWS_GET_RESPONSE.equals(returnType) || CLASS_AWS_GET_IS_RESPONSE.equals(returnType)) { - if (firstParameter != null && CommonUtils.isCollection(firstParameter.asType())) { - throw new ProcessingErrorException("@S3.Get operation expected single result, but parameter is collection of keys", method); + var parameterType = TypeName.get(parameter.asType()); + if (parameterType.equals(TypeName.INT)) { + return CodeBlock.of("$T.min(1000, $N)", Math.class, parameter.getSimpleName()); } - - String clientField = mode == S3Operation.Mode.SYNC - ? "_awsSyncClient" - : "_awsAsyncClient"; - - codeBuilder - .addStatement(key.code()) - .add("\n") - .addStatement(CodeBlock.of(""" - var _request = $T.builder() - .bucket(_clientConfig.bucket()) - .key(_key) - .build()""", CLASS_AWS_GET_REQUEST)) - .add("\n"); - - if (isMono) { - codeBuilder.beginControlFlow("return $T.fromCompletionStage(() -> ", CommonClassNames.mono); + if (parameterType.equals(ClassName.get(Integer.class))) { + return CodeBlock.of("$T.min(1000, $N)", Math.class, parameter.getSimpleName()); } - - if (mode == S3Operation.Mode.SYNC) { - if (isOptional) { - codeBuilder.beginControlFlow("try"); - codeBuilder.add("return $T.of(", Optional.class); - } else { - codeBuilder.add("return "); - } - - if (CLASS_AWS_GET_RESPONSE.equals(returnType)) { - codeBuilder.add("$L.getObject(_request).response()", clientField); - } else { - codeBuilder.add("$L.getObject(_request)", clientField); - } - - if (isOptional) { - codeBuilder - .addStatement(")") - .nextControlFlow("catch($T | $T e)", CLASS_AWS_EXCEPTION_NO_KEY, CLASS_AWS_EXCEPTION_NO_BUCKET) - .addStatement("return $T.empty()", Optional.class) - .endControlFlow(); - } else { - codeBuilder.add(";\n"); - } - } else { - if (CLASS_AWS_GET_RESPONSE.equals(returnType)) { - codeBuilder.add("return $L.getObject(_request, $T.toBlockingInputStream()).thenApply(_r -> _r.response())", - clientField, CLASS_AWS_IS_ASYNC_TRANSFORMER); - } else { - codeBuilder.add("return $L.getObject(_request, $T.toBlockingInputStream())", - clientField, CLASS_AWS_IS_ASYNC_TRANSFORMER); + if (parameterType.equals(TypeName.LONG) || parameterType.equals(ClassName.get(Long.class))) { + return CodeBlock.of("$T.min(1000, $T.toIntExact($N))", Math.class, parameter.getSimpleName()); + } + throw new ProcessingErrorException("@S3.List.Limit annotation can't have parameter of type %s: only int is allowed".formatted(parameterType), parameter); + } + var onMethod = AnnotationUtils.findAnnotation(method, S3ClassNames.Annotation.LIST_LIMIT); + if (onMethod != null) { + var value = AnnotationUtils.parseAnnotationValueWithoutDefault(onMethod, "value"); + if (value != null) { + if (value <= 0) { + throw new ProcessingErrorException("@S3.List.Limit should be more than zero", method, onMethod); } - - if (isMono) { - codeBuilder - .add("\n") - .add(""" - .exceptionallyCompose(e -> { - Throwable cause = e; - if (e instanceof $T) { - cause = e.getCause(); - } - if (cause instanceof $T) { - return $T.completedFuture(null); - } else { - return $T.failedFuture(cause); - } - }); - """, CompletionException.class, CLASS_S3_EXCEPTION_NOT_FOUND, CompletableFuture.class, CompletableFuture.class); - } else if (isOptional) { - codeBuilder.add(";\n"); - } else { - codeBuilder.add(";\n"); + if (value > 1000) { + throw new ProcessingErrorException("@S3.List.Limit should be less than 1000", method, onMethod); } + return CodeBlock.of("$L", value); } + throw new ProcessingErrorException("@S3.List.Limit annotation must have value when annotating method", method, onMethod); + } + return CodeBlock.of("1000"); + } - if (isMono) { - codeBuilder.endControlFlow(")"); + private CodeBlock extractDelimiter(ExecutableElement method) { + var onParameter = method.getParameters() + .stream() + .filter(p -> AnnotationUtils.isAnnotationPresent(p, S3ClassNames.Annotation.LIST_DELIMITER)) + .toList(); + if (onParameter.size() > 1) { + throw new ProcessingErrorException("@S3.List operation expected single @S3.List.Delimiter parameter", method); + } + if (!onParameter.isEmpty()) { + var parameter = onParameter.get(0); + var annotation = AnnotationUtils.findAnnotation(parameter, S3ClassNames.Annotation.LIST_DELIMITER); + if (AnnotationUtils.parseAnnotationValueWithoutDefault(annotation, "value") != null) { + throw new ProcessingErrorException("@S3.List.Delimiter annotation can't have value when annotating parameter", parameter, annotation); } - - return new S3Operation(method, operationMeta.annotation, OperationType.GET, ImplType.AWS, mode, codeBuilder.build()); - } else { - if (firstParameter != null && CommonUtils.isCollection(firstParameter.asType())) { - throw new ProcessingErrorException("@S3.Get operation unsupported method return signature, expected any of List<%s>/List<%s>".formatted( - CLASS_S3_OBJECT.simpleName(), CLASS_S3_OBJECT_META.simpleName() - ), method); - } else { - throw new ProcessingErrorException("@S3.Get operation unsupported method return signature, expected any of %s/%s/%s/ResponseInputStream<%s>".formatted( - CLASS_S3_OBJECT.simpleName(), CLASS_S3_OBJECT_META.simpleName(), CLASS_AWS_GET_RESPONSE.simpleName(), CLASS_AWS_GET_RESPONSE.simpleName() - ), method); + var parameterType = TypeName.get(parameter.asType()); + if (parameterType.equals(ClassName.get(String.class))) { + return CodeBlock.of("$N", parameter.getSimpleName()); + } + throw new ProcessingErrorException("@S3.List.Delimiter annotation can't have parameter of type %s: only String is allowed".formatted(parameterType), parameter); + } + var onMethod = AnnotationUtils.findAnnotation(method, S3ClassNames.Annotation.LIST_DELIMITER); + if (onMethod != null) { + var value = AnnotationUtils.parseAnnotationValueWithoutDefault(onMethod, "value"); + if (value != null) { + return CodeBlock.of("$S", value); } + throw new ProcessingErrorException("@S3.List.Delimiter annotation must have value when annotating method", method, onMethod); } + return CodeBlock.of("(String) null"); } - private S3Operation operationLIST(ExecutableElement method, OperationMeta operationMeta, S3Operation.Mode mode) { - final String keyMapping = AnnotationUtils.parseAnnotationValueWithoutDefault(operationMeta.annotation, "value"); - final Key key; - final VariableElement firstParameter = method.getParameters().stream().findFirst().orElse(null); - if (keyMapping != null && !keyMapping.isBlank()) { - key = parseKey(method, keyMapping); - if (key.params().isEmpty() && !method.getParameters().isEmpty()) { - throw new ProcessingErrorException("@S3.List operation prefix template must use method arguments or they should be removed", method); - } - } else if (method.getParameters().size() > 1) { - throw new ProcessingErrorException("@S3.List operation can't have multiple method parameters for keys without key template", method); - } else if (method.getParameters().isEmpty()) { - key = null; - } else if (CommonUtils.isCollection(firstParameter.asType())) { - throw new ProcessingErrorException("@S3.List operation expected single result, but parameter is collection of keys", method); + private MethodSpec operationPUT(GenerateConfigResult configSpec, ExecutableElement method, AnnotationMirror operationAnnotation) { + var returnType = TypeName.get(method.getReturnType()); + if (!returnType.equals(TypeName.VOID) && !returnType.equals(S3ClassNames.S3_OBJECT_UPLOAD_RESULT)) { + throw new ProcessingErrorException("@S3.Put operation return type must be void or S3ObjectUploadResult", method); + } + var bodyParams = method.getParameters() + .stream() + .filter(p -> S3ClassNames.BODY_TYPES.contains(TypeName.get(p.asType()))) + .toList(); + if (bodyParams.size() != 1) { + throw new ProcessingErrorException("@S3.Put operation must have exactly one parameter of types S3Body, byte[] or InputStream", method); + } + var bodyParam = bodyParams.get(0); + var bodyParamType = TypeName.get(bodyParam.asType()); + var bucket = extractBucket(configSpec, method); + var key = extractKey(method, operationAnnotation, true); + var b = CommonUtils.overridingKeepAop(method) + .addStatement("var _bucket = $L", bucket) + .addStatement("var _key = $L", key); + if (bodyParamType.equals(S3ClassNames.S3_BODY)) { + b.addStatement("var _body = $L", bodyParam.getSimpleName()); + } else if (bodyParamType.equals(ArrayTypeName.of(TypeName.BYTE))) { + b.addStatement("var _body = $T.ofBytes($L)", S3ClassNames.S3_BODY, bodyParam.getSimpleName()); + } else if (bodyParamType.equals(ClassName.get(InputStream.class))) { + b.addStatement("var _body = $T.ofInputStream($L)", S3ClassNames.S3_BODY, bodyParam.getSimpleName()); } else { - key = new Key(CodeBlock.of("var _key = String.valueOf($L)", firstParameter.toString()), List.of(firstParameter)); + throw new IllegalStateException("not gonna happen"); } + b.addStatement("return this.client.put(_bucket, _key, _body)"); + return b.build(); + } - if (MethodUtils.isOptional(method)) { - throw new ProcessingErrorException("@S3.List operation, can't be return type Optional", method); + private MethodSpec operationDELETE(GenerateConfigResult configSpec, ExecutableElement method, AnnotationMirror operationAnnotation) { + var returnType = TypeName.get(method.getReturnType()); + if (returnType != TypeName.VOID) { + throw new ProcessingErrorException("@S3.Delete operation must return void", method); } - - boolean isMono = MethodUtils.isMono(method); - final Integer limit = AnnotationUtils.parseAnnotationValue(elements, operationMeta.annotation(), "limit"); - final String delimiter = AnnotationUtils.parseAnnotationValueWithoutDefault(operationMeta.annotation(), "delimiter"); - final TypeName returnType = (mode == S3Operation.Mode.SYNC) - ? ClassName.get(method.getReturnType()) - : ClassName.get(MethodUtils.getGenericType(method.getReturnType()).orElseThrow()); - - var codeBuilder = CodeBlock.builder(); - for (VariableElement parameter : method.getParameters()) { - if(!(method.getReturnType() instanceof PrimitiveType)) { - codeBuilder.add(""" - if($T.isNull($L)) { - throw new $T("S3.List request prefix argument expected, but was null for arg: $L"); - } - """, Objects.class, parameter.getSimpleName().toString(), IllegalArgumentException.class, parameter.getSimpleName().toString()); - - } + var bucket = extractBucket(configSpec, method); + var nonBucketParams = method.getParameters().stream() + .filter(p -> !AnnotationUtils.isAnnotationPresent(p, S3ClassNames.Annotation.BUCKET)) + .toList(); + if (nonBucketParams.isEmpty()) { + throw new ProcessingErrorException("@S3.Delete operation must have key related parameter", method); } - codeBuilder.add("\n"); - - if (CLASS_S3_OBJECT_LIST.equals(returnType) || CLASS_S3_OBJECT_META_LIST.equals(returnType)) { - if (key != null) { - codeBuilder.addStatement(key.code()); - } - - if (isMono) { - codeBuilder.beginControlFlow("return $T.fromCompletionStage(() -> ", CommonClassNames.mono); - } - - var bodyBuilder = CodeBlock.builder(); - if (mode == S3Operation.Mode.SYNC) { - bodyBuilder.add("return _simpleSyncClient"); - } else { - bodyBuilder.add("return _simpleAsyncClient"); - } - - String keyField = (key == null) ? "(String) null" : "_key"; - if (CLASS_S3_OBJECT_LIST.equals(returnType)) { - bodyBuilder.add(".list(_clientConfig.bucket(), $L, $S, $L)", keyField, delimiter, limit); + var methodSpec = CommonUtils.overridingKeepAop(method) + .addStatement("var _bucket = $L", bucket); + var firstKeyParam = nonBucketParams.get(0); + var firstKeyParamType = firstKeyParam.asType(); + if (nonBucketParams.size() == 1 && (CommonUtils.isList(firstKeyParamType) || CommonUtils.isCollection(firstKeyParamType))) { + var collectionTypeName = (ParameterizedTypeName) TypeName.get(firstKeyParam.asType()); + if (collectionTypeName.typeArguments.get(0).equals(ClassName.get(String.class))) { + methodSpec.addStatement("var _key = $L", firstKeyParam); } else { - bodyBuilder.add(".listMeta(_clientConfig.bucket(), $L, $S, $L)", keyField, delimiter, limit); - } - - if (mode == S3Operation.Mode.ASYNC && CompletableFuture.class.getCanonicalName().equals(((DeclaredType) method.getReturnType()).asElement().toString())) { - bodyBuilder.add(".toCompletableFuture()"); - } - bodyBuilder.add(";\n"); - - codeBuilder.add(bodyBuilder.build()); - - if (isMono) { - codeBuilder.endControlFlow(")"); - } - - return new S3Operation(method, operationMeta.annotation, OperationType.LIST, ImplType.SIMPLE, mode, codeBuilder.build()); - } else if (CLASS_AWS_LIST_RESPONSE.equals(returnType)) { - String clientField = mode == S3Operation.Mode.SYNC - ? "_awsSyncClient" - : "_awsAsyncClient"; - - if (key != null) { - codeBuilder.addStatement(key.code()).add("\n"); - } - - String keyField = (key == null) ? "null" : "_key"; - codeBuilder - .addStatement(CodeBlock.of(""" - var _request = $L.builder() - .bucket(_clientConfig.bucket()) - .prefix($L) - .delimiter($S) - .maxKeys($L) - .build()""", CLASS_AWS_LIST_REQUEST, keyField, delimiter, limit)) - .add("\n"); - - if (MethodUtils.isMono(method)) { - codeBuilder.beginControlFlow("return $T.fromCompletionStage(() -> ", CommonClassNames.mono); - } - - codeBuilder - .addStatement("return $L.listObjectsV2(_request)", clientField) - .build(); - - if (MethodUtils.isMono(method)) { - codeBuilder.endControlFlow(")"); + methodSpec.addStatement("var _key = $L.stream().map($T::toString).toList()", firstKeyParam, Objects.class); } - - return new S3Operation(method, operationMeta.annotation, OperationType.LIST, ImplType.AWS, mode, codeBuilder.build()); } else { - throw new ProcessingErrorException("@S3.List operation unsupported method return signature, expected any of %s/%s/%s".formatted( - CLASS_S3_OBJECT.simpleName(), CLASS_S3_OBJECT_LIST.simpleName(), CLASS_AWS_LIST_RESPONSE.simpleName() - ), method); + var key = extractKey(method, operationAnnotation, true); + methodSpec.addStatement("var _key = $L", key); } + + methodSpec.addStatement("this.client.delete(_bucket, _key)"); + // todo map results maybe + return methodSpec.build(); } - private S3Operation operationPUT(ExecutableElement method, OperationMeta operationMeta, S3Operation.Mode mode) { - final String keyMapping = AnnotationUtils.parseAnnotationValueWithoutDefault(operationMeta.annotation, "value"); - final Key key; + record Key(CodeBlock code, List params) {} - var keyParameters = method.getParameters().stream() + private CodeBlock extractKey(ExecutableElement method, AnnotationMirror annotation, boolean required) { + var keyMapping = AnnotationUtils.parseAnnotationValueWithoutDefault(annotation, "value"); + var parameters = method.getParameters() + .stream() .filter(p -> { - TypeName bodyType = ClassName.get(p.asType()); - return !CLASS_S3_BODY.equals(bodyType) - && !ClassName.get(ByteBuffer.class).equals(bodyType) - && !ArrayTypeName.get(byte[].class).equals(bodyType); + var parameterTypeName = TypeName.get(p.asType()); + return !AnnotationUtils.isAnnotationPresent(p, S3ClassNames.Annotation.BUCKET) + && !AnnotationUtils.isAnnotationPresent(p, S3ClassNames.Annotation.LIST_LIMIT) + && !AnnotationUtils.isAnnotationPresent(p, S3ClassNames.Annotation.LIST_DELIMITER) + && !S3ClassNames.RANGE_CLASSES.contains(parameterTypeName) + && !S3ClassNames.BODY_TYPES.contains(parameterTypeName) + ; }) .toList(); - - final VariableElement firstParameter = keyParameters.stream().findFirst().orElse(null); if (keyMapping != null && !keyMapping.isBlank()) { - key = parseKey(method, keyMapping); - if (key.params().isEmpty() && !keyParameters.isEmpty()) { - throw new ProcessingErrorException("@S3.Put operation key template must use method arguments or they should be removed", method); + var key = parseKey(method, parameters, keyMapping); + if (key.params().isEmpty() && !parameters.isEmpty()) { + throw new ProcessingErrorException("@S3 operation prefix template must use method arguments or they should be removed", method); } - } else if (CommonUtils.isCollection(firstParameter.asType())) { - throw new ProcessingErrorException("@S3.Put operation expected single result, but parameter is collection of keys", method); - } else { - key = new Key(CodeBlock.of("var _key = String.valueOf($L)", firstParameter.toString()), List.of(firstParameter)); + return key.code; } - - if (MethodUtils.isOptional(method)) { - throw new ProcessingErrorException("@S3.Put operation, can't be return type Optional", method); + if (parameters.size() > 1) { + throw new ProcessingErrorException("@S3 operation can't have multiple method parameters for keys without key template", method); } - - final TypeMirror returnTypeMirror = (mode == S3Operation.Mode.SYNC) - ? method.getReturnType() - : MethodUtils.getGenericType(method.getReturnType()).orElseThrow(); - final TypeName returnType = ClassName.get(returnTypeMirror); - - final VariableElement bodyParam = method.getParameters().stream() - .filter(p -> key.params().stream().noneMatch(kp -> p == kp)) - .findFirst() - .orElseThrow(() -> new ProcessingErrorException("@S3.Put operation body parameter not found", method)); - - TypeName bodyType = ClassName.get(bodyParam.asType()); - - final boolean isMono = MethodUtils.isMono(method); - final boolean isUploadResponse = CLASS_S3_UPLOAD.equals(returnType); - final boolean isAwsResponse = CLASS_AWS_PUT_RESPONSE.equals(returnType); - - var methodBuilder = CodeBlock.builder(); - for (VariableElement parameter : method.getParameters()) { - if(!(method.getReturnType() instanceof PrimitiveType)) { - methodBuilder.add(""" - if($T.isNull($L)) { - throw new $T("S3.Put request argument expected, but was null for arg: $L"); - } - """, Objects.class, parameter.getSimpleName().toString(), IllegalArgumentException.class, parameter.getSimpleName().toString()); - } - } - methodBuilder.add("\n"); - - if (CommonUtils.isVoid(returnTypeMirror) || isUploadResponse) { - CodeBlock bodyCode; - if (CLASS_S3_BODY.equals(bodyType)) { - bodyCode = CodeBlock.of("var _body = $L", bodyParam.getSimpleName().toString()); - } else { - final String methodCall; - if (ClassName.get(ByteBuffer.class).equals(bodyType)) { - methodCall = "ofBuffer"; - } else if (ArrayTypeName.get(byte[].class).equals(bodyType)) { - methodCall = "ofBytes"; - } else { - throw new ProcessingErrorException("@S3.Put operation body must be S3Body/bytes/ByteBuffer", method); - } - - final String type = AnnotationUtils.parseAnnotationValueWithoutDefault(operationMeta.annotation(), "type"); - final String encoding = AnnotationUtils.parseAnnotationValueWithoutDefault(operationMeta.annotation(), "encoding"); - if (type != null && encoding != null) { - bodyCode = CodeBlock.of("var _body = $T.$L($L, $S, $S)", - CLASS_S3_BODY, methodCall, bodyParam.getSimpleName().toString(), methodCall, type, encoding); - } else if (type != null) { - bodyCode = CodeBlock.of("var _body = $T.$L($L, $S)", - CLASS_S3_BODY, methodCall, bodyParam.getSimpleName().toString(), methodCall, type); - } else if (encoding != null) { - bodyCode = CodeBlock.of("var _body = $T.$L($L, null, $S)", - CLASS_S3_BODY, methodCall, bodyParam.getSimpleName().toString(), methodCall, encoding); - } else { - bodyCode = CodeBlock.of("var _body = $T.$L($L)", - CLASS_S3_BODY, methodCall, bodyParam.getSimpleName().toString()); - } - } - - if (mode == S3Operation.Mode.SYNC) { - if (isUploadResponse) { - methodBuilder.add("return _simpleSyncClient.put(_clientConfig.bucket(), _key, _body)"); - } else { - methodBuilder.add("_simpleSyncClient.put(_clientConfig.bucket(), _key, _body)"); - } - methodBuilder.add(";\n"); - } else { - methodBuilder.add("return _simpleAsyncClient.put(_clientConfig.bucket(), _key, _body)"); - if (!isUploadResponse) { - methodBuilder.add(".thenAccept(_v -> {})"); - } - if (CompletableFuture.class.getCanonicalName().equals(((DeclaredType) method.getReturnType()).asElement().toString())) { - methodBuilder.add(".toCompletableFuture()"); - } - methodBuilder.add(";\n"); - } - - var bodyBuilder = CodeBlock.builder() - .addStatement(key.code()) - .add("\n"); - - if (isMono) { - bodyBuilder.beginControlFlow("return $T.fromCompletionStage(() -> ", CommonClassNames.mono); - } - - bodyBuilder - .addStatement(bodyCode) - .add("\n") - .add(methodBuilder.build()); - - if (isMono) { - bodyBuilder.endControlFlow(")"); - } - - return new S3Operation(method, operationMeta.annotation, OperationType.PUT, ImplType.SIMPLE, mode, bodyBuilder.build()); - } else if (isAwsResponse) { - CodeBlock bodyCode; - var requestBuilder = CodeBlock.builder(); - final String type = AnnotationUtils.parseAnnotationValueWithoutDefault(operationMeta.annotation(), "type"); - final String encoding = AnnotationUtils.parseAnnotationValueWithoutDefault(operationMeta.annotation(), "encoding"); - final String bodyParamName = bodyParam.getSimpleName().toString(); - - if (CLASS_S3_BODY.equals(bodyType)) { - if (mode == S3Operation.Mode.SYNC) { - bodyCode = CodeBlock.builder() - .add("final $T _requestBody;\n", CLASS_AWS_IS_SYNC_BODY) - .beginControlFlow("if ($L instanceof $T _bb)", bodyParamName, CLASS_S3_BODY_BYTES) - .add("_requestBody = $T.fromBytes(_bb.bytes());\n", CLASS_AWS_IS_SYNC_BODY) - .nextControlFlow("else") - .add("_requestBody = $T.fromContentProvider(() -> $L.asInputStream(), $L.size(), $L.type());\n", CLASS_AWS_IS_SYNC_BODY, bodyParamName, bodyParamName, bodyParamName) - .endControlFlow() - .build(); - } else { - bodyCode = CodeBlock.builder() - .add("final $T _requestBody;\n", CLASS_AWS_IS_ASYNC_BODY) - .beginControlFlow("if ($L instanceof $T _bb)", bodyParamName, CLASS_S3_BODY_BYTES) - .add("_requestBody = $T.fromBytes(_bb.bytes());\n", CLASS_AWS_IS_ASYNC_BODY) - .nextControlFlow("else if($L instanceof $T _bb)", bodyParamName, CLASS_S3_BODY_PUBLISHER) - .add("_requestBody = $T.fromPublisher($T.flowPublisherToFlux(_bb.asPublisher()));\n", CLASS_AWS_IS_ASYNC_BODY, CLASS_JDK_FLOW_ADAPTER) - .nextControlFlow("else", bodyParamName, bodyParamName) - .add("final Long _bodySize = $L.size() > 0 ? $L.size() : null;\n", bodyParamName, bodyParamName) - .add("_requestBody = $T.fromInputStream($L.asInputStream(), _bodySize, _awsAsyncExecutor);\n", CLASS_AWS_IS_ASYNC_BODY, bodyParamName) - .endControlFlow() - .build(); - } - - requestBuilder.addStatement("_requestBuilder.contentLength($L.size() > 0 ? $L.size() : null)", bodyParamName, bodyParamName); - if (type != null) { - requestBuilder.addStatement("_requestBuilder.contentType($S)", type); - } else { - requestBuilder.addStatement("_requestBuilder.contentType($L.type())", bodyParamName); - } - if (encoding != null) { - requestBuilder.addStatement("_requestBuilder.contentEncoding($S)", encoding); - } else { - requestBuilder.addStatement("_requestBuilder.contentEncoding($L.encoding())", bodyParamName); - } - } else { - var awsBodyClass = mode == S3Operation.Mode.SYNC - ? CLASS_AWS_IS_SYNC_BODY - : CLASS_AWS_IS_ASYNC_BODY; - - if (ClassName.get(ByteBuffer.class).equals(bodyType)) { - bodyCode = CodeBlock.of("var _requestBody = $T.fromByteBuffer($L)", - awsBodyClass, bodyParamName); - requestBuilder.addStatement("_requestBuilder.contentLength($L.remaining())", bodyParamName); - } else if (ArrayTypeName.get(byte[].class).equals(bodyType)) { - bodyCode = CodeBlock.of("var _requestBody = $T.fromBytes($L)", - awsBodyClass, bodyParamName); - requestBuilder.addStatement("_requestBuilder.contentLength($L.length)", bodyParamName); - } else { - throw new ProcessingErrorException("@S3.Put operation body must be S3Body/bytes/ByteBuffer", method); - } - - if (type != null) { - requestBuilder.addStatement("_requestBuilder.contentType($S)", type); - } - if (encoding != null) { - requestBuilder.addStatement("_requestBuilder.contentEncoding($S)", encoding); - } - } - - methodBuilder - .addStatement(key.code()) - .add("\n") - .addStatement(""" - var _requestBuilder = $T.builder() - .bucket(_clientConfig.bucket()) - .key(_key)""", CLASS_AWS_PUT_REQUEST) - .add(requestBuilder.build()) - .add("\n") - .addStatement("var _request = _requestBuilder.build()") - .add("\n"); - - if (MethodUtils.isMono(method)) { - methodBuilder.beginControlFlow("return $T.fromCompletionStage(() -> ", CommonClassNames.mono); - } - - if (CLASS_S3_BODY.equals(bodyType) && mode == S3Operation.Mode.SYNC) { - methodBuilder.add(""" - if ($L instanceof $T pb) { - return _awsAsyncClient.putObject(_request, $T.fromPublisher($T.flowPublisherToFlux(pb.asPublisher()))).join(); - } else if($L.size() < 0 || $L.size() > _awsClientConfig.upload().partSize().toBytes()) { - final Long _bodySize = $L.size() > 0 ? $L.size() : null; - return _awsAsyncMultipartClient.putObject(_request, $T.fromInputStream($L.asInputStream(), _bodySize, _awsAsyncExecutor)).join(); - } - """, - bodyParamName, CLASS_S3_BODY_PUBLISHER, - CLASS_AWS_IS_ASYNC_BODY, CLASS_JDK_FLOW_ADAPTER, - bodyParamName, bodyParamName, - bodyParamName, bodyParamName, - CLASS_AWS_IS_ASYNC_BODY, bodyParamName); - } - - methodBuilder - .add("\n") - .add(bodyCode) - .add("\n"); - - if (mode == S3Operation.Mode.ASYNC) { - methodBuilder - .addStatement(""" - return ($L.size() > 0 && $L.size() <= _awsClientConfig.upload().partSize().toBytes()) - ? _awsAsyncClient.putObject(_request, _requestBody) - : _awsAsyncMultipartClient.putObject(_request, _requestBody)""", bodyParamName, bodyParamName); + if (parameters.isEmpty()) { + if (required) { + throw new ProcessingErrorException("@S3 operation must have at least one method parameter for keys", method); } else { - methodBuilder.addStatement("return _awsSyncClient.putObject(_request, _requestBody)"); - } - - if (MethodUtils.isMono(method)) { - methodBuilder.endControlFlow(")"); + return CodeBlock.of("(String) null"); } + } - return new S3Operation(method, operationMeta.annotation, OperationType.PUT, ImplType.AWS, mode, methodBuilder.build()); + var firstParameter = parameters.get(0); + if (CommonUtils.isCollection(firstParameter.asType())) { + throw new ProcessingErrorException("@%s operation expected single result, but parameter is collection of keys".formatted(annotation), method); } else { - throw new ProcessingErrorException("@S3.Put operation unsupported method return signature, expected any of Void/%s/%s".formatted( - CLASS_S3_UPLOAD.simpleName(), CLASS_AWS_PUT_RESPONSE.simpleName() - ), method); + return CodeBlock.of("String.valueOf($N)", firstParameter.toString()); } } - private S3Operation operationDELETE(ExecutableElement method, OperationMeta operationMeta, S3Operation.Mode mode) { - final String keyMapping = AnnotationUtils.parseAnnotationValueWithoutDefault(operationMeta.annotation, "value"); - final Key key; - final VariableElement firstParameter = method.getParameters().stream().findFirst().orElse(null); - if (keyMapping != null && !keyMapping.isBlank()) { - key = parseKey(method, keyMapping); - if (key.params().isEmpty() && !method.getParameters().isEmpty()) { - throw new ProcessingErrorException("@S3.Delete operation key template must use method arguments or they should be removed", method); - } - } else if (method.getParameters().size() > 1) { - throw new ProcessingErrorException("@S3.Delete operation can't have multiple method parameters for keys without key template", method); - } else if (method.getParameters().isEmpty()) { - throw new ProcessingErrorException("@S3.Delete operation must have key parameter", method); - } else { - key = new Key(CodeBlock.of("var _key = String.valueOf($L)", firstParameter.toString()), List.of(firstParameter)); + private CodeBlock extractBucket(GenerateConfigResult configSpec, ExecutableElement method) { + var onParameter = method.getParameters() + .stream() + .filter(p -> AnnotationUtils.isAnnotationPresent(p, S3ClassNames.Annotation.BUCKET)) + .toList(); + if (onParameter.size() > 1) { + throw new ProcessingErrorException("@S3 operation can't have multiple method parameters for bucket", method); } - - if (MethodUtils.isOptional(method)) { - throw new ProcessingErrorException("@S3.Delete operation, can't be return type Optional", method); + if (onParameter.size() == 1) { + return CodeBlock.of("$N", onParameter.get(0).getSimpleName()); } - - final boolean isMono = MethodUtils.isMono(method); - final TypeMirror returnTypeMirror = (mode == S3Operation.Mode.SYNC) - ? method.getReturnType() - : MethodUtils.getGenericType(method.getReturnType()).orElseThrow(); - final TypeName returnType = ClassName.get(returnTypeMirror); - - var methodBuilder = CodeBlock.builder(); - for (VariableElement parameter : method.getParameters()) { - if(!(method.getReturnType() instanceof PrimitiveType)) { - methodBuilder.add(""" - if($T.isNull($L)) { - throw new $T("S3.Delete request key argument expected, but was null for arg: $L"); - } - """, Objects.class, parameter.getSimpleName().toString(), IllegalArgumentException.class, parameter.getSimpleName().toString()); - } + var onMethod = AnnotationUtils.findAnnotation(method, S3ClassNames.Annotation.BUCKET); + if (onMethod != null) { + var value = AnnotationUtils.parseAnnotationValueWithoutDefault(onMethod, "value"); + var i = configSpec.paths.indexOf(value); + if (i < 0) { + throw new ProcessingErrorException("@S3 operation must have bucket parameter or bucket value in config", method); + } + return CodeBlock.of("this.config.bucket_$L", i); } - methodBuilder.add("\n"); - - if (CommonUtils.isVoid(returnTypeMirror)) { - String clientField = mode == S3Operation.Mode.SYNC - ? "_simpleSyncClient" - : "_simpleAsyncClient"; - - final String keyArgName; - boolean isKeyCollection = firstParameter != null && CommonUtils.isCollection(firstParameter.asType()); - if (isKeyCollection) { - keyArgName = firstParameter.getSimpleName().toString(); - } else { - methodBuilder.addStatement(key.code()); - keyArgName = "_key"; - } - - if (isMono) { - methodBuilder.beginControlFlow("return $T.fromCompletionStage(() -> ", CommonClassNames.mono); - } - - if (mode == S3Operation.Mode.ASYNC) { - methodBuilder.add("return "); - } - methodBuilder.add("$L.delete(_clientConfig.bucket(), $L)", clientField, keyArgName); - if (mode == S3Operation.Mode.ASYNC) { - if (CompletableFuture.class.getCanonicalName().equals(((DeclaredType) method.getReturnType()).asElement().toString())) { - methodBuilder.add(".toCompletableFuture()"); - } - } - methodBuilder.add(";\n"); - - if (isMono) { - methodBuilder.endControlFlow(")"); - } - - return new S3Operation(method, operationMeta.annotation, OperationType.DELETE, ImplType.SIMPLE, mode, methodBuilder.build()); - } else if (CLASS_AWS_DELETE_RESPONSE.equals(returnType)) { - if (firstParameter != null && CommonUtils.isCollection(firstParameter.asType())) { - throw new ProcessingErrorException("@S3.Delete operation expected single result, but parameter is collection of keys", method); - } - - String clientField = mode == S3Operation.Mode.SYNC - ? "_awsSyncClient" - : "_awsAsyncClient"; - - methodBuilder - .addStatement(key.code()) - .add("\n") - .addStatement(CodeBlock.of(""" - var _request = $T.builder() - .bucket(_clientConfig.bucket()) - .key(_key) - .build()""", CLASS_AWS_DELETE_REQUEST)) - .add("\n"); - - if (MethodUtils.isMono(method)) { - methodBuilder.beginControlFlow("return $T.fromCompletionStage(() -> ", CommonClassNames.mono); - } - - methodBuilder - .addStatement("return $L.deleteObject(_request)", clientField) - .build(); - - if (MethodUtils.isMono(method)) { - methodBuilder.endControlFlow(")"); - } - - return new S3Operation(method, operationMeta.annotation, OperationType.DELETE, ImplType.AWS, mode, methodBuilder.build()); - } else if (CLASS_AWS_DELETES_RESPONSE.equals(returnType)) { - if (firstParameter == null || !CommonUtils.isCollection(firstParameter.asType())) { - throw new ProcessingErrorException("@S3.Delete operation multiple keys, but parameter is not collection of keys", method); - } - - String clientField = mode == S3Operation.Mode.SYNC - ? "_awsSyncClient" - : "_awsAsyncClient"; - - methodBuilder.addStatement(CodeBlock.of(""" - var _request = $T.builder() - .bucket(_clientConfig.bucket()) - .delete($T.builder() - .objects($L.stream() - .map(k -> $T.builder() - .key(k) - .build()) - .toList()) - .build()) - .build()""", CLASS_AWS_DELETES_REQUEST, - ClassName.get("software.amazon.awssdk.services.s3.model", "Delete"), - firstParameter.getSimpleName().toString(), - ClassName.get("software.amazon.awssdk.services.s3.model", "ObjectIdentifier"))) - .add("\n"); - - if (MethodUtils.isMono(method)) { - methodBuilder.beginControlFlow("return $T.fromCompletionStage(() -> ", CommonClassNames.mono); - } - - methodBuilder - .addStatement("return $L.deleteObjects(_request)", clientField) - .build(); - - if (MethodUtils.isMono(method)) { - methodBuilder.endControlFlow(")"); - } - - return new S3Operation(method, operationMeta.annotation, OperationType.DELETE, ImplType.AWS, mode, methodBuilder.build()); - } else { - throw new ProcessingErrorException("@S3.Delete operation unsupported method return signature, expected any of Void/%s/%s".formatted( - CLASS_AWS_DELETE_RESPONSE.simpleName(), CLASS_AWS_DELETES_RESPONSE.simpleName() - ), method); + var onClass = AnnotationUtils.findAnnotation(method.getEnclosingElement(), S3ClassNames.Annotation.BUCKET); + if (onClass != null) { + var value = AnnotationUtils.parseAnnotationValueWithoutDefault(onClass, "value"); + var i = configSpec.paths.indexOf(value); + if (i < 0) { + throw new ProcessingErrorException("@S3 operation must have bucket parameter or bucket value in config", method); + } + return CodeBlock.of("this.config.bucket_$L", i); } + throw new ProcessingErrorException("S3 operation expected bucket on parameter, method or class but got none", method); } - record Key(CodeBlock code, List params) {} - - private Key parseKey(ExecutableElement method, String keyTemplate) { + private Key parseKey(ExecutableElement method, List parameters, String keyTemplate) { int indexStart = keyTemplate.indexOf("{"); if (indexStart == -1) { - return new Key(CodeBlock.of("var _key = $S", keyTemplate), Collections.emptyList()); + return new Key(CodeBlock.of("$S", keyTemplate), Collections.emptyList()); } - List params = new ArrayList<>(); - CodeBlock.Builder builder = CodeBlock.builder(); - builder.add("var _key = "); + var params = new ArrayList(); + var builder = CodeBlock.builder(); int indexEnd = 0; while (indexStart != -1) { if (indexStart != 0) { @@ -1044,13 +578,8 @@ private Key parseKey(ExecutableElement method, String keyTemplate) { } indexEnd = keyTemplate.indexOf("}", indexStart); - final String paramName = keyTemplate.substring(indexStart + 1, indexEnd); - final VariableElement parameter = method.getParameters().stream() - .filter(p -> { - TypeName bodyType = ClassName.get(p.asType()); - return !ClassName.get(ByteBuffer.class).equals(bodyType) - && !ArrayTypeName.get(byte[].class).equals(bodyType); - }) + var paramName = keyTemplate.substring(indexStart + 1, indexEnd); + var parameter = parameters.stream() .filter(p -> p.getSimpleName().contentEquals(paramName)) .findFirst() .orElseThrow(() -> new ProcessingErrorException("@S3 operation key part named '%s' expected, but wasn't found".formatted(paramName), method)); diff --git a/experimental/s3-client-annotation-processor/src/main/java/ru/tinkoff/kora/s3/client/annotation/processor/S3Operation.java b/experimental/s3-client-annotation-processor/src/main/java/ru/tinkoff/kora/s3/client/annotation/processor/S3Operation.java deleted file mode 100644 index 47350a5f0..000000000 --- a/experimental/s3-client-annotation-processor/src/main/java/ru/tinkoff/kora/s3/client/annotation/processor/S3Operation.java +++ /dev/null @@ -1,32 +0,0 @@ -package ru.tinkoff.kora.s3.client.annotation.processor; - -import com.squareup.javapoet.CodeBlock; - -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; - -public record S3Operation(ExecutableElement method, - AnnotationMirror annotation, - OperationType type, - ImplType impl, - Mode mode, - CodeBlock code) { - - public enum Mode { - ASYNC, - SYNC - } - - public enum OperationType { - GET, - LIST, - PUT, - DELETE - } - - public enum ImplType { - SIMPLE, - AWS, - MINIO - } -} diff --git a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/AbstractS3Test.java b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/AbstractS3Test.java new file mode 100644 index 000000000..d8cc6a02c --- /dev/null +++ b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/AbstractS3Test.java @@ -0,0 +1,42 @@ +package ru.tinkoff.kora.s3.client.annotation.processor; + +import org.intellij.lang.annotations.Language; +import org.mockito.Mockito; +import ru.tinkoff.kora.annotation.processor.common.AbstractAnnotationProcessorTest; +import ru.tinkoff.kora.aop.annotation.processor.AopAnnotationProcessor; +import ru.tinkoff.kora.s3.client.S3Client; +import ru.tinkoff.kora.s3.client.S3ClientFactory; + +import java.util.ArrayList; +import java.util.List; + +public class AbstractS3Test extends AbstractAnnotationProcessorTest { + @Override + protected String commonImports() { + return super.commonImports() + """ + import java.nio.ByteBuffer; + import java.io.InputStream; + import java.util.List; + import java.util.Iterator; + import java.util.Collection; + import java.util.Optional; + import ru.tinkoff.kora.s3.client.annotation.*; + import ru.tinkoff.kora.s3.client.annotation.S3.*; + import ru.tinkoff.kora.s3.client.model.*; + import ru.tinkoff.kora.s3.client.*; + import ru.tinkoff.kora.s3.client.S3Client.*; + """; + } + + protected S3Client s3Client = Mockito.mock(S3Client.class); + + protected TestObject compile(@Language("java") String source, Object... addArgs) { + var result = this.compile(List.of(new S3ClientAnnotationProcessor(), new AopAnnotationProcessor()), source); + result.assertSuccess(); + var clientFactory = (S3ClientFactory) s3ClientClazz -> s3Client; + var args = new ArrayList(1 + addArgs.length); + args.add(clientFactory); + args.addAll(List.of(addArgs)); + return new TestObject(loadClass("$Client_Impl"), args); + } +} diff --git a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3AwsAsyncClientTests.java b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3AwsAsyncClientTests.java deleted file mode 100644 index 2fa6a6174..000000000 --- a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3AwsAsyncClientTests.java +++ /dev/null @@ -1,207 +0,0 @@ -package ru.tinkoff.kora.s3.client.annotation.processor; - -import org.junit.jupiter.api.Test; -import ru.tinkoff.kora.annotation.processor.common.AbstractAnnotationProcessorTest; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -class S3AwsAsyncClientTests extends AbstractAnnotationProcessorTest { - - @Override - protected String commonImports() { - return super.commonImports() + """ - import java.util.concurrent.CompletableFuture; - import java.util.concurrent.CompletionStage; - import java.util.List; - import java.util.Collection; - import ru.tinkoff.kora.s3.client.annotation.*; - import ru.tinkoff.kora.s3.client.annotation.S3.*; - import ru.tinkoff.kora.s3.client.model.*; - import ru.tinkoff.kora.s3.client.*; - import ru.tinkoff.kora.s3.client.model.S3Object; - import software.amazon.awssdk.services.s3.model.*; - """; - } - - @Test - public void clientGetAws() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - CompletionStage get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetAwsFuture() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - CompletableFuture get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListAws() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - CompletionStage list(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListAwsWithPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - CompletionStage list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListAwsLimit() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List(limit = 100) - CompletionStage list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyAndDelimiter() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List(value = "{key1}", delimiter = "/") - CompletionStage list(String key1); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListAwsFuture() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - CompletableFuture list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteAws() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - CompletionStage delete(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteAwsFuture() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - CompletableFuture delete(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeletesAws() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - CompletionStage delete(List key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBody() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - CompletionStage put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyFuture() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - CompletableFuture put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } -} diff --git a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3AwsClientTests.java b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3AwsClientTests.java deleted file mode 100644 index dd117678d..000000000 --- a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3AwsClientTests.java +++ /dev/null @@ -1,192 +0,0 @@ -package ru.tinkoff.kora.s3.client.annotation.processor; - -import org.junit.jupiter.api.Test; -import ru.tinkoff.kora.annotation.processor.common.AbstractAnnotationProcessorTest; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -class S3AwsClientTests extends AbstractAnnotationProcessorTest { - - @Override - protected String commonImports() { - return super.commonImports() + """ - import java.util.List; - import java.util.Collection; - import java.util.Optional; - import ru.tinkoff.kora.s3.client.annotation.*; - import ru.tinkoff.kora.s3.client.annotation.S3.*; - import ru.tinkoff.kora.s3.client.model.*; - import ru.tinkoff.kora.s3.client.*; - import ru.tinkoff.kora.s3.client.model.S3Object; - import software.amazon.awssdk.services.s3.model.*; - import software.amazon.awssdk.core.ResponseInputStream; - """; - } - - @Test - public void clientGetAws() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - GetObjectResponse get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetAwsIS() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - ResponseInputStream get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetAwsOptional() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - Optional get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetAwsOptionalIS() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - Optional> get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListAws() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - ListObjectsV2Response list(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListAwsWithPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - ListObjectsV2Response list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListAwsLimit() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List(limit = 100) - ListObjectsV2Response list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyAndDelimiter() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List(value = "{key1}", delimiter = "/") - ListObjectsV2Response list(String key1); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteAws() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - DeleteObjectResponse delete(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeletesAws() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - DeleteObjectsResponse delete(List key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBody() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - PutObjectResponse put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } -} diff --git a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3AwsReactorClientTests.java b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3AwsReactorClientTests.java deleted file mode 100644 index ecc97739f..000000000 --- a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3AwsReactorClientTests.java +++ /dev/null @@ -1,148 +0,0 @@ -package ru.tinkoff.kora.s3.client.annotation.processor; - -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import ru.tinkoff.kora.annotation.processor.common.AbstractAnnotationProcessorTest; - -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import static org.assertj.core.api.Assertions.assertThat; - -class S3AwsReactorClientTests extends AbstractAnnotationProcessorTest { - - @Override - protected String commonImports() { - return super.commonImports() + """ - import reactor.core.publisher.Mono; - import java.util.List; - import java.util.Collection; - import ru.tinkoff.kora.s3.client.annotation.*; - import ru.tinkoff.kora.s3.client.annotation.S3.*; - import ru.tinkoff.kora.s3.client.model.*; - import ru.tinkoff.kora.s3.client.*; - import ru.tinkoff.kora.s3.client.model.S3Object; - import software.amazon.awssdk.services.s3.model.*; - """; - } - - @Test - public void clientGetAws() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - Mono get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListAws() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - Mono list(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListAwsWithPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - Mono list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListAwsLimit() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List(limit = 100) - Mono list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyAndDelimiter() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List(value = "{key1}", delimiter = "/") - Mono list(String key1); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteAws() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - Mono delete(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeletesAws() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - Mono delete(List key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBody() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - Mono put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } -} diff --git a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3DeleteTest.java b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3DeleteTest.java new file mode 100644 index 000000000..cd0b2b0b0 --- /dev/null +++ b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3DeleteTest.java @@ -0,0 +1,66 @@ +package ru.tinkoff.kora.s3.client.annotation.processor; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +public class S3DeleteTest extends AbstractS3Test { + @Test + public void testDeleteKey() { + var client = this.compile(""" + @S3.Client(clientFactoryTag = String.class) + public interface Client { + @S3.Delete + void deleteKey(@S3.Bucket String bucket, String key); + } + """); + + var key1 = UUID.randomUUID().toString(); + client.invoke("deleteKey", "bucket", key1); + verify(s3Client).delete("bucket", key1); + reset(s3Client); + } + + @Test + public void testDeleteKeyWithTemplate() { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.Delete("some/prefix/{key1}/{key2}") + void deleteKey(@S3.Bucket String bucket, String key1, String key2); + } + """); + + var key1 = UUID.randomUUID().toString(); + var key2 = UUID.randomUUID().toString(); + client.invoke("deleteKey", "bucket", key1, key2); + verify(s3Client).delete("bucket", "some/prefix/" + key1 + "/" + key2); + reset(s3Client); + } + + @Test + public void testDeleteKeys() { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.Delete + void deleteKeys(@S3.Bucket String bucket, List key); + + @S3.Delete + void deleteKeysNonString(@S3.Bucket String bucket, List key); + } + """); + + var key1 = UUID.randomUUID().toString(); + var key2 = UUID.randomUUID().toString(); + + client.invoke("deleteKeys", "bucket", List.of(key1, key2)); + verify(s3Client).delete("bucket", List.of(key1, key2)); + reset(s3Client); + } + +} diff --git a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3GetTest.java b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3GetTest.java new file mode 100644 index 000000000..5f6e540c7 --- /dev/null +++ b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3GetTest.java @@ -0,0 +1,364 @@ +package ru.tinkoff.kora.s3.client.annotation.processor; + +import org.junit.jupiter.api.Test; +import ru.tinkoff.kora.config.common.factory.MapConfigFactory; +import ru.tinkoff.kora.s3.client.model.S3Body; +import ru.tinkoff.kora.s3.client.model.S3Object; +import ru.tinkoff.kora.s3.client.model.S3ObjectMeta; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Supplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class S3GetTest extends AbstractS3Test { + + @Test + public void testGetMetadata() { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.Get + S3ObjectMeta getRequired(@S3.Bucket String bucket, String key); + + @S3.Get + Optional getOptional(@S3.Bucket String bucket, String key); + + @S3.Get + @Nullable + S3ObjectMeta getNullable(@S3.Bucket String bucket, String key); + } + """); + + var meta = new S3ObjectMeta("bucket", "key", Instant.now(), -1); + + when(s3Client.getMeta("bucket", "key")).thenReturn(meta); + assertThat(client.invoke("getRequired", "bucket", "key")).isSameAs(meta); + verify(s3Client).getMeta("bucket", "key"); + reset(s3Client); + + when(s3Client.getMetaOptional("bucket", "key")).thenReturn(meta); + assertThat((Optional) client.invoke("getOptional", "bucket", "key")).containsSame(meta); + verify(s3Client).getMetaOptional("bucket", "key"); + reset(s3Client); + + when(s3Client.getMetaOptional("bucket", "key")).thenReturn(null); + assertThat((Optional) client.invoke("getOptional", "bucket", "key")).isEmpty(); + verify(s3Client).getMetaOptional("bucket", "key"); + reset(s3Client); + + when(s3Client.getMetaOptional("bucket", "key")).thenReturn(meta); + assertThat(client.invoke("getNullable", "bucket", "key")).isSameAs(meta); + verify(s3Client).getMetaOptional("bucket", "key"); + reset(s3Client); + + when(s3Client.getMetaOptional("bucket", "key")).thenReturn(null); + assertThat(client.invoke("getNullable", "bucket", "key")).isNull(); + verify(s3Client).getMetaOptional("bucket", "key"); + reset(s3Client); + } + + @Test + public void testGetBytes() { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.Get + byte[] get(@S3.Bucket String bucket, String key); + + @S3.Get + byte[] getRange(@S3.Bucket String bucket, String key, RangeData range); + + @S3.Get + @Nullable + byte[] getNullable(@S3.Bucket String bucket, String key); + + @S3.Get + Optional getOptional(@S3.Bucket String bucket, String key); + } + """); + + var meta = new S3ObjectMeta("bucket", "key", Instant.now(), -1); + var bytes = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8); + var supplier = (Supplier) () -> new S3Object(meta, S3Body.ofInputStream(new ByteArrayInputStream(bytes), bytes.length)); + + when(s3Client.get("bucket", "key", null)).thenReturn(supplier.get()); + assertThat((byte[]) client.invoke("get", "bucket", "key")).isEqualTo(bytes); + verify(s3Client).get("bucket", "key", null); + reset(s3Client); + + when(s3Client.getOptional("bucket", "key", null)).thenReturn(supplier.get()); + assertThat(client.>invoke("getOptional", "bucket", "key")).contains(bytes); + verify(s3Client).getOptional("bucket", "key", null); + reset(s3Client); + + when(s3Client.getOptional("bucket", "key", null)).thenReturn(null); + assertThat(client.>invoke("getOptional", "bucket", "key")).isEmpty(); + verify(s3Client).getOptional("bucket", "key", null); + reset(s3Client); + + when(s3Client.getOptional("bucket", "key", null)).thenReturn(supplier.get()); + assertThat(client.invoke("getNullable", "bucket", "key")).isEqualTo(bytes); + verify(s3Client).getOptional("bucket", "key", null); + reset(s3Client); + + when(s3Client.getOptional("bucket", "key", null)).thenReturn(null); + assertThat(client.invoke("getNullable", "bucket", "key")).isNull(); + verify(s3Client).getOptional("bucket", "key", null); + reset(s3Client); + } + + @Test + public void testGetObject() throws Exception { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.Get + S3Object get(@Bucket String bucket, String key); + + @S3.Get + S3Object getRange(@S3.Bucket String bucket, String key, RangeData range); + + @S3.Get + @Nullable + S3Object getNullable(@Bucket String bucket, String key); + } + """); + + var meta = new S3ObjectMeta("bucket", "key", Instant.now(), -1); + var bytes = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8); + var supplier = (Supplier) () -> new S3Object(meta, S3Body.ofInputStream(new ByteArrayInputStream(bytes), bytes.length)); + + when(s3Client.get("bucket", "key", null)).thenReturn(supplier.get()); + try (var object = (S3Object) client.invoke("get", "bucket", "key")) { + assertThat(object).isNotNull(); + assertThat(object.meta()).isEqualTo(meta); + try (var body = object.body()) { + assertThat(body.asBytes()).isEqualTo(bytes); + } + } + verify(s3Client).get("bucket", "key", null); + reset(s3Client); + + when(s3Client.getOptional("bucket", "key", null)).thenReturn(supplier.get()); + try (var object = (S3Object) client.invoke("getNullable", "bucket", "key")) { + assertThat(object).isNotNull(); + assertThat(object.meta()).isEqualTo(meta); + try (var body = object.body()) { + assertThat(body.asBytes()).isEqualTo(bytes); + } + } + verify(s3Client).getOptional("bucket", "key", null); + reset(s3Client); + + when(s3Client.getOptional("bucket", "key", null)).thenReturn(null); + try (var object = (S3Object) client.invoke("getNullable", "bucket", "key")) { + assertThat(object).isNull(); + } + verify(s3Client).getOptional("bucket", "key", null); + reset(s3Client); + } + + @Test + public void testGetS3Body() throws Exception { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.Get + S3Body get(@Bucket String bucket, String key); + + @S3.Get + S3Body getRange(@S3.Bucket String bucket, String key, RangeData range); + + @S3.Get + @Nullable + S3Body getNullable(@Bucket String bucket, String key); + } + """); + + var meta = new S3ObjectMeta("bucket", "key", Instant.now(), -1); + var bytes = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8); + var supplier = (Supplier) () -> new S3Object(meta, S3Body.ofInputStream(new ByteArrayInputStream(bytes), bytes.length)); + + when(s3Client.get("bucket", "key", null)).thenReturn(supplier.get()); + try (var body = (S3Body) client.invoke("get", "bucket", "key")) { + assertThat(body.asBytes()).isEqualTo(bytes); + } + verify(s3Client).get("bucket", "key", null); + reset(s3Client); + + when(s3Client.getOptional("bucket", "key", null)).thenReturn(supplier.get()); + try (var body = (S3Body) client.invoke("getNullable", "bucket", "key")) { + assertThat(body.asBytes()).isEqualTo(bytes); + } + verify(s3Client).getOptional("bucket", "key", null); + reset(s3Client); + + when(s3Client.getOptional("bucket", "key", null)).thenReturn(null); + try (var body = (S3Body) client.invoke("getNullable", "bucket", "key")) { + assertThat(body).isNull(); + } + verify(s3Client).getOptional("bucket", "key", null); + reset(s3Client); + } + + @Test + public void testGetInputStream() throws Exception { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.Get + InputStream get(@Bucket String bucket, String key); + + @S3.Get + InputStream getRange(@S3.Bucket String bucket, String key, RangeData range); + + @S3.Get + @Nullable + InputStream getNullable(@Bucket String bucket, String key); + } + """); + + var meta = new S3ObjectMeta("bucket", "key", Instant.now(), -1); + var bytes = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8); + var supplier = (Supplier) () -> new S3Object(meta, S3Body.ofInputStream(new ByteArrayInputStream(bytes), bytes.length)); + + when(s3Client.get("bucket", "key", null)).thenReturn(supplier.get()); + try (var body = (InputStream) client.invoke("get", "bucket", "key")) { + assertThat(body.readAllBytes()).isEqualTo(bytes); + } + verify(s3Client).get("bucket", "key", null); + reset(s3Client); + + when(s3Client.getOptional("bucket", "key", null)).thenReturn(supplier.get()); + try (var body = (InputStream) client.invoke("getNullable", "bucket", "key")) { + assertThat(body.readAllBytes()).isEqualTo(bytes); + } + verify(s3Client).getOptional("bucket", "key", null); + reset(s3Client); + + when(s3Client.getOptional("bucket", "key", null)).thenReturn(null); + try (var body = (InputStream) client.invoke("getNullable", "bucket", "key")) { + assertThat(body).isNull(); + } + verify(s3Client).getOptional("bucket", "key", null); + reset(s3Client); + } + + @Test + public void testGetWithKeyTemplate() { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.Get("prefix/{key}/suffix") + byte[] get(@S3.Bucket String bucket, String key); + } + """); + + var meta = new S3ObjectMeta("bucket", "key", Instant.now(), -1); + var bytes = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8); + var supplier = (Supplier) () -> new S3Object(meta, S3Body.ofInputStream(new ByteArrayInputStream(bytes), bytes.length)); + + when(s3Client.get("bucket", "prefix/key/suffix", null)).thenReturn(supplier.get()); + assertThat((byte[]) client.invoke("get", "bucket", "key")).isEqualTo(bytes); + verify(s3Client).get("bucket", "prefix/key/suffix", null); + reset(s3Client); + } + + @Test + public void testGetWithBucketOnClient() { + var config = MapConfigFactory.fromMap(Map.of( + "s3", Map.of( + "client", Map.of( + "bucket", "on-class" + ) + ) + )); + var client = this.compile(""" + @S3.Client + @Bucket("s3.client.bucket") + public interface Client { + @S3.Get("prefix/{key}/suffix") + byte[] get(String key); + } + """, newGeneratedObject("$Client_ClientConfig", config)); + + var meta = new S3ObjectMeta("on-class", "key", Instant.now(), -1); + var bytes = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8); + var supplier = (Supplier) () -> new S3Object(meta, S3Body.ofInputStream(new ByteArrayInputStream(bytes), bytes.length)); + + when(s3Client.get("on-class", "prefix/key/suffix", null)).thenReturn(supplier.get()); + assertThat(client.invoke("get", "key")).isEqualTo(bytes); + verify(s3Client).get("on-class", "prefix/key/suffix", null); + reset(s3Client); + } + + @Test + public void testGetWithBucketOnMethod() { + var config = MapConfigFactory.fromMap(Map.of( + "s3", Map.of( + "client", Map.of( + "get", Map.of( + "bucket", "on-method" + ) + ) + ) + )); + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.Get("prefix/{key}/suffix") + @Bucket("s3.client.get.bucket") + byte[] get(String key); + } + """, newGeneratedObject("$Client_ClientConfig", config)); + + var meta = new S3ObjectMeta("on-method", "key", Instant.now(), -1); + var bytes = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8); + var supplier = (Supplier) () -> new S3Object(meta, S3Body.ofInputStream(new ByteArrayInputStream(bytes), bytes.length)); + + when(s3Client.get("on-method", "prefix/key/suffix", null)).thenReturn(supplier.get()); + assertThat(client.invoke("get", "key")).isEqualTo(bytes); + verify(s3Client).get("on-method", "prefix/key/suffix", null); + reset(s3Client); + } + + @Test + public void testGetWithBucketOnMethodAndType() { + var config = MapConfigFactory.fromMap(Map.of( + "s3", Map.of( + "client", Map.of( + "bucket", "on-class", + "get", Map.of( + "bucket", "on-method" + ) + ) + ) + )); + var client = this.compile(""" + @S3.Client + @Bucket("s3.client.bucket") + public interface Client { + @S3.Get("prefix/{key}/suffix") + @Bucket("s3.client.get.bucket") + byte[] get(String key); + } + """, newGeneratedObject("$Client_ClientConfig", config)); + + var meta = new S3ObjectMeta("on-method", "key", Instant.now(), -1); + var bytes = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8); + var supplier = (Supplier) () -> new S3Object(meta, S3Body.ofInputStream(new ByteArrayInputStream(bytes), bytes.length)); + + when(s3Client.get("on-method", "prefix/key/suffix", null)).thenReturn(supplier.get()); + assertThat((byte[]) client.invoke("get", "key")).isEqualTo(bytes); + verify(s3Client).get("on-method", "prefix/key/suffix", null); + reset(s3Client); + } +} diff --git a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3KoraAsyncClientTests.java b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3KoraAsyncClientTests.java deleted file mode 100644 index fb72393de..000000000 --- a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3KoraAsyncClientTests.java +++ /dev/null @@ -1,644 +0,0 @@ -package ru.tinkoff.kora.s3.client.annotation.processor; - -import org.junit.jupiter.api.Test; -import ru.tinkoff.kora.annotation.processor.common.AbstractAnnotationProcessorTest; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class S3KoraAsyncClientTests extends AbstractAnnotationProcessorTest { - - @Override - protected String commonImports() { - return super.commonImports() + """ - import java.util.concurrent.CompletionStage; - import java.util.concurrent.CompletableFuture; - import java.nio.ByteBuffer; - import java.io.InputStream; - import java.util.List; - import java.util.Collection; - import ru.tinkoff.kora.s3.client.annotation.*; - import ru.tinkoff.kora.s3.client.annotation.S3.*; - import ru.tinkoff.kora.s3.client.model.*; - import ru.tinkoff.kora.s3.client.*; - import ru.tinkoff.kora.s3.client.model.S3Object; - import software.amazon.awssdk.services.s3.model.*; - """; - } - - @Test - public void clientConfig() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - CompletionStage get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - - var config = this.compileResult.loadClass("$Client_ClientConfigModule"); - assertThat(config).isNotNull(); - } - - // Get - @Test - public void clientGetMeta() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - CompletionStage get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetMetaFuture() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - CompletableFuture get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetObject() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - CompletionStage get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetManyMetas() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - CompletionStage> get(Collection keys); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetManyMetasFuture() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - CompletableFuture> get(Collection keys); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetManyObjects() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - CompletionStage> get(List keys); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetManyObjectsFuture() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - CompletableFuture> get(List keys); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetKeyConcat() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("{key1}-{key2}") - CompletionStage get(String key1, long key2); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetKeyMissing() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("{key1}-{key12345}") - CompletionStage get(String key1); - } - """)); - } - - @Test - public void clientGetKeyConst() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("const-key") - CompletionStage get(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetKeyUnused() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("const-key") - CompletionStage get(String key); - } - """)); - } - - // List - @Test - public void clientListMeta() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - CompletionStage list(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListMetaFuture() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - CompletableFuture list(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListMetaWithPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - CompletionStage list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListMetaFutureWithPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - CompletableFuture list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListObjectsWithPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - CompletionStage list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListLimit() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List(limit = 100) - CompletionStage list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyConcat() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List("{key1}-{key2}") - CompletionStage list(String key1, long key2); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyAndDelimiter() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List(value = "{key1}", delimiter = "/") - CompletionStage list(String key1); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyMissing() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List("{key1}-{key12345}") - CompletionStage list(String key1); - } - """)); - } - - @Test - public void clientListKeyConst() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List("const-key") - CompletionStage list(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyUnused() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List("const-key") - CompletionStage list(String key); - } - """)); - } - - // Delete - @Test - public void clientDelete() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - CompletionStage delete(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteFuture() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - CompletableFuture delete(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteKeyConcat() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete("{key1}-{key2}") - CompletionStage delete(String key1, long key2); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteKeyMissing() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete("{key1}-{key12345}") - CompletionStage delete(String key1); - } - """)); - } - - @Test - public void clientDeleteKeyConst() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete("const-key") - CompletionStage delete(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteKeyUnused() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete("const-key") - CompletionStage delete(String key); - } - """)); - } - - // Deletes - @Test - public void clientDeletes() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - CompletionStage delete(List key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - // Put - @Test - public void clientPutBody() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - CompletionStage put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyFuture() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - CompletableFuture put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyReturnUpload() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - CompletionStage put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBytes() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - CompletionStage put(String key, byte[] body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBuffer() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - CompletionStage put(String key, ByteBuffer body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyAndType() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put(type = "type") - CompletionStage put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyAndEncoding() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put(encoding = "encoding") - CompletionStage put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyAndTypeAndEncoding() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put(type = "type", encoding = "encoding") - CompletionStage put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutKeyConcat() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put("{key1}-{key2}") - CompletionStage put(String key1, long key2, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutKeyMissing() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put("{key1}-{key12345}") - CompletionStage put(String key1, S3Body body); - } - """)); - } - - @Test - public void clientPutKeyConst() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put("const-key") - CompletionStage put(S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutKeyUnused() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put("const-key") - CompletionStage put(String key, S3Body body); - } - """)); - } -} diff --git a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3KoraClientTests.java b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3KoraClientTests.java deleted file mode 100644 index a4ec9ef08..000000000 --- a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3KoraClientTests.java +++ /dev/null @@ -1,611 +0,0 @@ -package ru.tinkoff.kora.s3.client.annotation.processor; - -import org.junit.jupiter.api.Test; -import ru.tinkoff.kora.annotation.processor.common.AbstractAnnotationProcessorTest; - -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class S3KoraClientTests extends AbstractAnnotationProcessorTest { - - @Override - protected String commonImports() { - return super.commonImports() + """ - import java.nio.ByteBuffer; - import java.io.InputStream; - import java.util.List; - import java.util.Collection; - import java.util.Optional; - import ru.tinkoff.kora.s3.client.annotation.*; - import ru.tinkoff.kora.s3.client.annotation.S3.*; - import ru.tinkoff.kora.s3.client.model.*; - import ru.tinkoff.kora.s3.client.*; - import ru.tinkoff.kora.s3.client.model.S3Object; - import software.amazon.awssdk.services.s3.model.*; - """; - } - - // Get - @Test - public void clientGetMeta() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - S3ObjectMeta get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetMetaOptional() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - Optional get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetObject() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - S3Object get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetObjectOptional() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - Optional get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetManyMetas() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - List get(Collection keys); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetManyObjects() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - List get(List keys); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetKeyConcat() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("{key1}-{key2}") - S3ObjectMeta get(String key1, long key2); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetKeyMissing() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("{key1}-{key12345}") - S3ObjectMeta get(String key1); - } - """)); - } - - @Test - public void clientGetKeyConst() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("const-key") - S3ObjectMeta get(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetKeyPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("pre-{key1}") - S3ObjectMeta get(String key1); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetKeySuffix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("{key1}-suffix") - S3ObjectMeta get(String key1); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetKeyPrefixSuffix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("pre-{key1}-suffix") - S3ObjectMeta get(String key1); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetKeyUnused() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("const-key") - S3ObjectMeta get(String key); - } - """)); - } - - // List - @Test - public void clientListMeta() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - S3ObjectMetaList list(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListObjects() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - S3ObjectList list(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListMetaWithPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - S3ObjectMetaList list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListObjectsWithPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - S3ObjectList list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListLimitWithPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List(limit = 100) - S3ObjectList list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyConcat() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List("{key1}-{key2}") - S3ObjectList list(String key1, long key2); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyAndDelimiter() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List(value = "{key1}", delimiter = "/") - S3ObjectList list(String key1); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyMissing() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List("{key1}-{key12345}") - S3ObjectList list(String key1); - } - """)); - } - - @Test - public void clientListKeyConst() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List("const-key") - S3ObjectList list(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyUnused() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List("const-key") - S3ObjectList list(String key); - } - """)); - } - - // Delete - @Test - public void clientDelete() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - void delete(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteKeyConcat() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete("{key1}-{key2}") - void delete(String key1, long key2); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteKeyMissing() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete("{key1}-{key12345}") - void delete(String key1); - } - """)); - } - - @Test - public void clientDeleteKeyConst() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete("const-key") - void delete(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteKeyUnused() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete("const-key") - void delete(String key); - } - """)); - } - - // Deletes - @Test - public void clientDeletes() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - void delete(List key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - // Put - @Test - public void clientPutBody() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - void put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyReturnUpload() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - S3ObjectUpload put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBytes() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - void put(String key, byte[] body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBuffer() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - void put(String key, ByteBuffer body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyAndType() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put(type = "type") - void put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyAndEncoding() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put(encoding = "encoding") - void put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyAndTypeAndEncoding() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put(type = "type", encoding = "encoding") - void put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutKeyConcat() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put("{key1}-{key2}") - void put(String key1, long key2, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutKeyMissing() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put("{key1}-{key12345}") - void put(String key1, S3Body body); - } - """)); - } - - @Test - public void clientPutKeyConst() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put("const-key") - void put(S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutKeyUnused() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put("const-key") - void put(String key, S3Body body); - } - """)); - } -} diff --git a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3KoraReactorClientTests.java b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3KoraReactorClientTests.java deleted file mode 100644 index 3d39b3155..000000000 --- a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3KoraReactorClientTests.java +++ /dev/null @@ -1,553 +0,0 @@ -package ru.tinkoff.kora.s3.client.annotation.processor; - -import org.junit.jupiter.api.Test; -import ru.tinkoff.kora.annotation.processor.common.AbstractAnnotationProcessorTest; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class S3KoraReactorClientTests extends AbstractAnnotationProcessorTest { - - @Override - protected String commonImports() { - return super.commonImports() + """ - import reactor.core.publisher.Mono; - import java.nio.ByteBuffer; - import java.io.InputStream; - import java.util.List; - import java.util.Collection; - import ru.tinkoff.kora.s3.client.annotation.*; - import ru.tinkoff.kora.s3.client.annotation.S3.*; - import ru.tinkoff.kora.s3.client.model.*; - import ru.tinkoff.kora.s3.client.*; - import ru.tinkoff.kora.s3.client.model.S3Object; - import software.amazon.awssdk.services.s3.model.*; - """; - } - - @Test - public void clientConfig() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - Mono get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - - var config = this.compileResult.loadClass("$Client_ClientConfigModule"); - assertThat(config).isNotNull(); - } - - // Get - @Test - public void clientGetMeta() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - Mono get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetObject() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - Mono get(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetManyMetas() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - Mono> get(Collection keys); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetManyObjects() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get - Mono> get(List keys); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetKeyConcat() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("{key1}-{key2}") - Mono get(String key1, long key2); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetKeyMissing() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("{key1}-{key12345}") - Mono get(String key1); - } - """)); - } - - @Test - public void clientGetKeyConst() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("const-key") - Mono get(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientGetKeyUnused() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Get("const-key") - Mono get(String key); - } - """)); - } - - // List - @Test - public void clientListMeta() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - Mono list(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListMetaWithPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - Mono list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListMetaFutureWithPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - Mono list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListObjectsWithPrefix() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List - Mono list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListLimit() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List(limit = 100) - Mono list(String prefix); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyConcat() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List("{key1}-{key2}") - Mono list(String key1, long key2); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyAndDelimiter() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List(value = "{key1}", delimiter = "/") - Mono list(String key1); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyMissing() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List("{key1}-{key12345}") - Mono list(String key1); - } - """)); - } - - @Test - public void clientListKeyConst() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List("const-key") - Mono list(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientListKeyUnused() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.List("const-key") - Mono list(String key); - } - """)); - } - - // Delete - @Test - public void clientDelete() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - Mono delete(String key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteKeyConcat() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete("{key1}-{key2}") - Mono delete(String key1, long key2); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteKeyMissing() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete("{key1}-{key12345}") - Mono delete(String key1); - } - """)); - } - - @Test - public void clientDeleteKeyConst() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete("const-key") - Mono delete(); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientDeleteKeyUnused() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete("const-key") - Mono delete(String key); - } - """)); - } - - // Deletes - @Test - public void clientDeletes() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Delete - Mono delete(List key); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - // Put - @Test - public void clientPutBody() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - Mono put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyReturnUpload() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - Mono put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBytes() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - Mono put(String key, byte[] body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBuffer() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put - Mono put(String key, ByteBuffer body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyAndType() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put(type = "type") - Mono put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyAndEncoding() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put(encoding = "encoding") - Mono put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutBodyAndTypeAndEncoding() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put(type = "type", encoding = "encoding") - Mono put(String key, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutKeyConcat() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put("{key1}-{key2}") - Mono put(String key1, long key2, S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutKeyMissing() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put("{key1}-{key12345}") - Mono put(String key1, S3Body body); - } - """)); - } - - @Test - public void clientPutKeyConst() { - this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put("const-key") - Mono put(S3Body body); - } - """); - this.compileResult.assertSuccess(); - var clazz = this.compileResult.loadClass("$Client_Impl"); - assertThat(clazz).isNotNull(); - } - - @Test - public void clientPutKeyUnused() { - assertThatThrownBy(() -> this.compile(List.of(new S3ClientAnnotationProcessor()), """ - @S3.Client("my") - public interface Client { - - @S3.Put("const-key") - Mono put(String key, S3Body body); - } - """)); - } -} diff --git a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3ListTest.java b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3ListTest.java new file mode 100644 index 000000000..aa7e302ac --- /dev/null +++ b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3ListTest.java @@ -0,0 +1,157 @@ +package ru.tinkoff.kora.s3.client.annotation.processor; + +import org.junit.jupiter.api.Test; +import ru.tinkoff.kora.s3.client.model.S3ObjectMeta; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +public class S3ListTest extends AbstractS3Test { + @Test + public void testListBucket() { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.List + List list(@S3.Bucket String bucket); + + @S3.List + Iterator iterator(@S3.Bucket String bucket); + } + """); + + var list = List.of(); + + when(s3Client.list("bucket", null, null, 1000)).thenReturn(list); + assertThat(client.>invoke("list", "bucket")).isSameAs(list); + verify(s3Client).list("bucket", null, null, 1000); + reset(s3Client); + + var iterator = new ArrayList().iterator(); + when(s3Client.listIterator("bucket", null, null, 1000)).thenReturn(iterator); + assertThat(client.>invoke("iterator", "bucket")).isSameAs(iterator); + verify(s3Client).listIterator("bucket", null, null, 1000); + reset(s3Client); + } + + @Test + public void testListBucketPrefix() { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.List + List prefix(@S3.Bucket String bucket, String prefix); + + @S3.List("/prefix/{prefix}") + List template(@S3.Bucket String bucket, String prefix); + } + """); + + var list = List.of(); + + when(s3Client.list("bucket", "test1", null, 1000)).thenReturn(list); + assertThat(client.>invoke("prefix", "bucket", "test1")).isSameAs(list); + verify(s3Client).list("bucket", "test1", null, 1000); + reset(s3Client); + + when(s3Client.list("bucket", "/prefix/test1", null, 1000)).thenReturn(list); + assertThat(client.>invoke("template", "bucket", "test1")).isSameAs(list); + verify(s3Client).list("bucket", "/prefix/test1", null, 1000); + reset(s3Client); + + } + + @Test + public void testListLimit() { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.List + List listOnParameter(@S3.Bucket String bucket, @S3.List.Limit int limit); + + @S3.List + Iterator iteratorOnParameter(@S3.Bucket String bucket, @S3.List.Limit int limit); + + @S3.List + @S3.List.Limit(44) + List listOnMethod(@S3.Bucket String bucket); + + @S3.List + @S3.List.Limit(45) + Iterator iteratorOnMethod(@S3.Bucket String bucket); + } + """); + + var list = List.of(); + var iterator = new ArrayList().iterator(); + + when(s3Client.list("bucket", null, null, 42)).thenReturn(list); + assertThat(client.>invoke("listOnParameter", "bucket", 42)).isSameAs(list); + verify(s3Client).list("bucket", null, null, 42); + reset(s3Client); + + when(s3Client.listIterator("bucket", null, null, 43)).thenReturn(iterator); + assertThat(client.>invoke("iteratorOnParameter", "bucket", 43)).isSameAs(iterator); + verify(s3Client).listIterator("bucket", null, null, 43); + reset(s3Client); + + when(s3Client.list("bucket", null, null, 44)).thenReturn(list); + assertThat(client.>invoke("listOnMethod", "bucket")).isSameAs(list); + verify(s3Client).list("bucket", null, null, 44); + reset(s3Client); + + when(s3Client.listIterator("bucket", null, null, 45)).thenReturn(iterator); + assertThat(client.>invoke("iteratorOnMethod", "bucket")).isSameAs(iterator); + verify(s3Client).listIterator("bucket", null, null, 45); + reset(s3Client); + } + + @Test + public void testListDelimiter() { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.List + List listOnParameter(@S3.Bucket String bucket, @S3.List.Delimiter String delimiter); + + @S3.List + Iterator iteratorOnParameter(@S3.Bucket String bucket, @S3.List.Delimiter String delimiter); + + @S3.List + @S3.List.Delimiter("/") + List listOnMethod(@S3.Bucket String bucket); + + @S3.List + @S3.List.Delimiter("/") + Iterator iteratorOnMethod(@S3.Bucket String bucket); + } + """); + + var list = List.of(); + var iterator = new ArrayList().iterator(); + + when(s3Client.list("bucket", null, "/", 1000)).thenReturn(list); + assertThat(client.>invoke("listOnParameter", "bucket", "/")).isSameAs(list); + verify(s3Client).list("bucket", null, "/", 1000); + reset(s3Client); + + when(s3Client.listIterator("bucket", null, "/", 1000)).thenReturn(iterator); + assertThat(client.>invoke("iteratorOnParameter", "bucket", "/")).isSameAs(iterator); + verify(s3Client).listIterator("bucket", null, "/", 1000); + reset(s3Client); + + when(s3Client.list("bucket", null, "/", 1000)).thenReturn(list); + assertThat(client.>invoke("listOnMethod", "bucket")).isSameAs(list); + verify(s3Client).list("bucket", null, "/", 1000); + reset(s3Client); + + when(s3Client.listIterator("bucket", null, "/", 1000)).thenReturn(iterator); + assertThat(client.>invoke("iteratorOnMethod", "bucket")).isSameAs(iterator); + verify(s3Client).listIterator("bucket", null, "/", 1000); + reset(s3Client); + } +} diff --git a/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3PutTest.java b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3PutTest.java new file mode 100644 index 000000000..e469031c0 --- /dev/null +++ b/experimental/s3-client-annotation-processor/src/test/java/ru/tinkoff/kora/s3/client/annotation/processor/S3PutTest.java @@ -0,0 +1,54 @@ +package ru.tinkoff.kora.s3.client.annotation.processor; + +import org.junit.jupiter.api.Test; +import ru.tinkoff.kora.s3.client.model.S3Body; +import ru.tinkoff.kora.s3.client.model.S3ObjectUploadResult; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class S3PutTest extends AbstractS3Test { + + @Test + public void testPutByteArray() { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.Put + S3ObjectUploadResult put(@S3.Bucket String bucket, String key, byte[] data); + } + """); + + var meta = new S3ObjectUploadResult("bucket", "key", "etag", "version"); + var bytes = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8); + + when(s3Client.put(eq("bucket"), eq("key"), eq(S3Body.ofBytes(bytes)))).thenReturn(meta); + assertThat(client.invoke("put", "bucket", "key", bytes)).isSameAs(meta); + verify(s3Client).put(eq("bucket"), eq("key"), eq(S3Body.ofBytes(bytes))); + reset(s3Client); + } + + @Test + public void testPutByteWithKeyTemplate() { + var client = this.compile(""" + @S3.Client + public interface Client { + @S3.Put("prefix/{key}/suffix") + S3ObjectUploadResult put(@S3.Bucket String bucket, String key, byte[] data); + } + """); + + var meta = new S3ObjectUploadResult("bucket", "key", "etag", "version"); + var bytes = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8); + + when(s3Client.put(eq("bucket"), eq("prefix/key/suffix"), eq(S3Body.ofBytes(bytes)))).thenReturn(meta); + assertThat(client.invoke("put", "bucket", "key", bytes)).isSameAs(meta); + verify(s3Client).put(eq("bucket"), eq("prefix/key/suffix"), eq(S3Body.ofBytes(bytes))); + reset(s3Client); + } + + +} diff --git a/experimental/s3-client-aws/build.gradle b/experimental/s3-client-aws/build.gradle deleted file mode 100644 index 9b241c953..000000000 --- a/experimental/s3-client-aws/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -apply from: "${project.rootDir}/gradle/in-test-generated.gradle" - -dependencies { - annotationProcessor project(":config:config-annotation-processor") - - compileOnly project(":http:http-client-common") - compileOnly libs.jetbrains.annotations - - api libs.reactor.core - api project(":experimental:s3-client-common") - api(libs.s3client.aws) { - exclude group: "software.amazon.awssdk", module: "apache-client" - exclude group: "software.amazon.awssdk", module: "netty-nio-client" - } - - implementation project(":config:config-common") - - testImplementation project(":internal:test-logging") - testImplementation libs.testcontainers.junit.jupiter -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3BodyAsync.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3BodyAsync.java deleted file mode 100644 index e20813b3e..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3BodyAsync.java +++ /dev/null @@ -1,57 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.s3.client.model.S3Body; - -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.concurrent.Flow; - -@ApiStatus.Experimental -final class AwsS3BodyAsync implements S3Body { - - private final String encoding; - private final String type; - private final long size; - private final Flow.Publisher publisher; - - public AwsS3BodyAsync(String encoding, String type, long size, Flow.Publisher publisher) { - this.encoding = encoding; - this.type = type; - this.size = size; - this.publisher = publisher; - } - - @Override - public InputStream asInputStream() { - return S3Body.ofPublisher(publisher, size, type, encoding).asInputStream(); - } - - @Override - public Flow.Publisher asPublisher() { - return publisher; - } - - @Override - public long size() { - return size; - } - - @Override - public String encoding() { - return encoding; - } - - @Override - public String type() { - return type; - } - - @Override - public String toString() { - return "AwsS3Body{type=" + type + - ", encoding=" + encoding + - ", size=" + size + - '}'; - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3BodySync.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3BodySync.java deleted file mode 100644 index 4e288e08d..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3BodySync.java +++ /dev/null @@ -1,58 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.s3.client.model.S3Body; - -import java.io.InputStream; -import java.net.http.HttpRequest; -import java.nio.ByteBuffer; -import java.util.concurrent.Flow; - -@ApiStatus.Experimental -final class AwsS3BodySync implements S3Body { - - private final String encoding; - private final String type; - private final long size; - private final InputStream inputStream; - - public AwsS3BodySync(String encoding, String type, long size, InputStream inputStream) { - this.encoding = encoding; - this.type = type; - this.size = size; - this.inputStream = inputStream; - } - - @Override - public InputStream asInputStream() { - return this.inputStream; - } - - @Override - public Flow.Publisher asPublisher() { - return HttpRequest.BodyPublishers.ofInputStream(() -> inputStream); - } - - @Override - public long size() { - return size; - } - - @Override - public String encoding() { - return encoding; - } - - @Override - public String type() { - return type; - } - - @Override - public String toString() { - return "AwsS3Body{type=" + type + - ", encoding=" + encoding + - ", size=" + size + - '}'; - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ClientConfig.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ClientConfig.java deleted file mode 100644 index 858dd0357..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ClientConfig.java +++ /dev/null @@ -1,47 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.common.util.Size; -import ru.tinkoff.kora.config.common.annotation.ConfigValueExtractor; - -import java.time.Duration; - -@ApiStatus.Experimental -@ConfigValueExtractor -public interface AwsS3ClientConfig { - - enum AddressStyle { - PATH, - VIRTUAL_HOSTED - } - - default AddressStyle addressStyle() { - return AddressStyle.PATH; - } - - default Duration requestTimeout() { - return Duration.ofSeconds(45); - } - - default boolean checksumValidationEnabled() { - return false; - } - - default boolean chunkedEncodingEnabled() { - return true; - } - - UploadConfig upload(); - - @ConfigValueExtractor - interface UploadConfig { - - default Size bufferSize() { - return Size.of(32, Size.Type.MiB); - } - - default Size partSize() { - return Size.of(8, Size.Type.MiB); - } - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ClientModule.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ClientModule.java deleted file mode 100644 index 0d37aec7c..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ClientModule.java +++ /dev/null @@ -1,139 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.application.graph.All; -import ru.tinkoff.kora.common.DefaultComponent; -import ru.tinkoff.kora.common.Tag; -import ru.tinkoff.kora.config.common.Config; -import ru.tinkoff.kora.config.common.extractor.ConfigValueExtractor; -import ru.tinkoff.kora.http.client.common.HttpClient; -import ru.tinkoff.kora.s3.client.S3ClientModule; -import ru.tinkoff.kora.s3.client.S3Config; -import ru.tinkoff.kora.s3.client.S3KoraAsyncClient; -import ru.tinkoff.kora.s3.client.S3KoraClient; -import ru.tinkoff.kora.s3.client.telemetry.S3ClientTelemetryFactory; -import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientTelemetryFactory; -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.awscore.AwsClient; -import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; -import software.amazon.awssdk.http.SdkHttpClient; -import software.amazon.awssdk.http.async.SdkAsyncHttpClient; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.s3.S3AsyncClient; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.S3Configuration; -import software.amazon.awssdk.services.s3.internal.multipart.MultipartS3AsyncClient; -import software.amazon.awssdk.services.s3.model.MultipartUpload; -import software.amazon.awssdk.services.s3.multipart.MultipartConfiguration; - -import java.net.URI; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -@ApiStatus.Experimental -public interface AwsS3ClientModule extends S3ClientModule { - - @Tag(AwsClient.class) - @DefaultComponent - default HttpClient awsS3httpClient(HttpClient client) { - return client; - } - - @DefaultComponent - default KoraAwsSdkHttpClient awsKoraSdkHttpClient(@Tag(AwsClient.class) HttpClient client, - AwsS3ClientConfig clientConfig) { - return new KoraAwsSdkHttpClient(client, clientConfig); - } - - @Tag(AwsClient.class) - @DefaultComponent - default ExecutorService awsAsyncExecutorService() { - return Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 2) * 2); - } - - default AwsS3ClientConfig awsS3ClientConfig(Config config, ConfigValueExtractor extractor) { - var value = config.get("s3client.aws"); - return extractor.extract(value); - } - - @DefaultComponent - default AwsCredentialsProvider awsCredentialsProvider(S3Config s3Config) { - return () -> AwsBasicCredentials.create(s3Config.accessKey(), s3Config.secretKey()); - } - - @DefaultComponent - default S3Configuration awsS3Configuration(AwsS3ClientConfig awsS3ClientConfig) { - return S3Configuration.builder() - .checksumValidationEnabled(awsS3ClientConfig.checksumValidationEnabled()) - .chunkedEncodingEnabled(awsS3ClientConfig.chunkedEncodingEnabled()) - .pathStyleAccessEnabled(awsS3ClientConfig.addressStyle() == AwsS3ClientConfig.AddressStyle.PATH) - .build(); - } - - default S3Client awsS3Client(SdkHttpClient httpClient, - AwsCredentialsProvider credentialsProvider, - S3Configuration s3Configuration, - S3Config s3Config, - AwsS3ClientConfig awsS3ClientConfig, - S3ClientTelemetryFactory telemetryFactory, - All interceptors) { - return S3Client.builder() - .credentialsProvider(credentialsProvider) - .httpClient(httpClient) - .endpointOverride(URI.create(s3Config.url())) - .serviceConfiguration(s3Configuration) - .region(Region.of(s3Config.region())) - .overrideConfiguration(b -> b.addExecutionInterceptor(new AwsS3ClientTelemetryInterceptor(telemetryFactory.get(s3Config.telemetry(), S3Client.class), awsS3ClientConfig.addressStyle()))) - .overrideConfiguration(b -> interceptors.forEach(b::addExecutionInterceptor)) - .build(); - } - - default S3AsyncClient awsS3AsyncClient(SdkAsyncHttpClient asyncHttpClient, - AwsCredentialsProvider credentialsProvider, - S3Configuration s3Configuration, - S3Config s3Config, - AwsS3ClientConfig awsS3ClientConfig, - S3ClientTelemetryFactory telemetryFactory, - All interceptors) { - return S3AsyncClient.builder() - .credentialsProvider(credentialsProvider) - .httpClient(asyncHttpClient) - .endpointOverride(URI.create(s3Config.url())) - .serviceConfiguration(s3Configuration) - .region(Region.of(s3Config.region())) - .overrideConfiguration(b -> b.addExecutionInterceptor(new AwsS3ClientTelemetryInterceptor(telemetryFactory.get(s3Config.telemetry(), S3AsyncClient.class), awsS3ClientConfig.addressStyle()))) - .overrideConfiguration(b -> interceptors.forEach(b::addExecutionInterceptor)) - .build(); - } - - default S3KoraClient awsS3KoraClient(S3Client s3Client, - S3KoraAsyncClient simpleAsyncClient, - S3KoraClientTelemetryFactory telemetryFactory, - S3Config config, - AwsS3ClientConfig awsS3ClientConfig) { - var telemetry = telemetryFactory.get(config.telemetry(), S3KoraClient.class); - return new AwsS3KoraClient(s3Client, simpleAsyncClient, telemetry, awsS3ClientConfig); - } - - default S3KoraAsyncClient awsS3KoraAsyncClient(S3AsyncClient s3AsyncClient, - @Tag(AwsClient.class) ExecutorService awsExecutor, - S3KoraClientTelemetryFactory telemetryFactory, - S3Config config, - AwsS3ClientConfig awsS3ClientConfig) { - var telemetry = telemetryFactory.get(config.telemetry(), S3KoraAsyncClient.class); - return new AwsS3KoraAsyncClient(s3AsyncClient, awsExecutor, telemetry, awsS3ClientConfig); - } - - @Tag(MultipartUpload.class) - default MultipartS3AsyncClient multipartS3AsyncClient(S3AsyncClient asyncClient, - AwsS3ClientConfig awsS3ClientConfig) { - MultipartConfiguration config = MultipartConfiguration.builder() - .thresholdInBytes(awsS3ClientConfig.upload().partSize().toBytes()) - .apiCallBufferSizeInBytes(awsS3ClientConfig.upload().bufferSize().toBytes()) - .minimumPartSizeInBytes(awsS3ClientConfig.upload().partSize().toBytes()) - .build(); - - return MultipartS3AsyncClient.create(asyncClient, config); - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ClientTelemetryInterceptor.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ClientTelemetryInterceptor.java deleted file mode 100644 index 3ce1c809a..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ClientTelemetryInterceptor.java +++ /dev/null @@ -1,145 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; -import ru.tinkoff.kora.s3.client.S3Exception; -import ru.tinkoff.kora.s3.client.telemetry.S3ClientTelemetry; -import ru.tinkoff.kora.s3.client.telemetry.S3ClientTelemetry.S3ClientTelemetryContext; -import software.amazon.awssdk.awscore.exception.AwsServiceException; -import software.amazon.awssdk.core.SdkResponse; -import software.amazon.awssdk.core.async.AsyncRequestBody; -import software.amazon.awssdk.core.interceptor.Context; -import software.amazon.awssdk.core.interceptor.ExecutionAttribute; -import software.amazon.awssdk.core.interceptor.ExecutionAttributes; -import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.http.SdkHttpRequest; -import software.amazon.awssdk.http.SdkHttpResponse; - -@ApiStatus.Experimental -public final class AwsS3ClientTelemetryInterceptor implements ExecutionInterceptor { - - record Operation(String method, String bucket, String key) {} - - private static final ExecutionAttribute CONTEXT = new ExecutionAttribute<>("kora-s3-telemetry-context"); - private static final ExecutionAttribute OPERATION = new ExecutionAttribute<>("kora-s3-telemetry-operation"); - - private final S3ClientTelemetry telemetry; - private final AwsS3ClientConfig.AddressStyle addressStyle; - - public AwsS3ClientTelemetryInterceptor(S3ClientTelemetry telemetry, AwsS3ClientConfig.AddressStyle addressStyle) { - this.telemetry = telemetry; - this.addressStyle = addressStyle; - } - - @Override - public void beforeExecution(Context.BeforeExecution execContext, ExecutionAttributes executionAttributes) { - final S3ClientTelemetryContext telemetryContext = telemetry.get(); - executionAttributes.putAttribute(CONTEXT, telemetryContext); - } - - @Override - public void afterMarshalling(Context.AfterMarshalling execContext, ExecutionAttributes executionAttributes) { - var telemetryContext = executionAttributes.getAttribute(CONTEXT); - if (telemetryContext != null) { - SdkHttpRequest request = execContext.httpRequest(); - Long contentLength = execContext.requestBody().flatMap(RequestBody::optionalContentLength) - .or(() -> execContext.asyncRequestBody().flatMap(AsyncRequestBody::contentLength)) - .orElse(null); - - final BucketAndKey bk; - if (addressStyle == AwsS3ClientConfig.AddressStyle.PATH) { - bk = getPathAddress(request.encodedPath()); - } else { - bk = getVirtualHost(request.host(), request.encodedPath()); - } - - telemetryContext.prepared(request.method().name(), bk.bucket(), bk.key(), contentLength); - executionAttributes.putAttribute(OPERATION, new Operation(request.method().name().toUpperCase(), bk.bucket(), bk.key())); - } - } - - private record BucketAndKey(String bucket, @Nullable String key) {} - - private static BucketAndKey getPathAddress(String path) { - final String bucket; - final String key; - final int startFrom = (path.charAt(0) == '/') - ? 1 - : 0; - - if (path.equals("/")) { - bucket = "/"; - key = null; - } else { - int bucketSeparator = path.indexOf('/', startFrom); - if (bucketSeparator == -1) { - bucket = path.substring(startFrom); - key = null; - } else { - bucket = path.substring(startFrom, bucketSeparator); - key = path.substring(bucketSeparator + 1); - } - } - - return new BucketAndKey(bucket, key); - } - - private static BucketAndKey getVirtualHost(String host, String path) { - int bucketEnd = host.indexOf('.'); - if (bucketEnd == -1) { - return getPathAddress(path); - } - - final String bucket; - final String key; - final int startFrom = (path.charAt(0) == '/') - ? 1 - : 0; - - bucket = host.substring(0, bucketEnd); - if (path.length() == 1) { - key = null; - } else { - key = path.substring(startFrom); - } - - return new BucketAndKey(bucket, key); - } - - @Override - public void afterExecution(Context.AfterExecution execContext, ExecutionAttributes executionAttributes) { - var telemetryContext = executionAttributes.getAttribute(CONTEXT); - if (telemetryContext != null) { - var httpResponse = execContext.response().sdkHttpResponse(); - var op = executionAttributes.getAttribute(OPERATION); - telemetryContext.close(op.method(), op.bucket(), op.key(), httpResponse.statusCode()); - } - } - - @Override - public void onExecutionFailure(Context.FailedExecution execContext, ExecutionAttributes executionAttributes) { - var telemetryContext = executionAttributes.getAttribute(CONTEXT); - if (telemetryContext != null) { - var statusCode = execContext.response() - .map(SdkResponse::sdkHttpResponse) - .map(SdkHttpResponse::statusCode) - .or(() -> execContext.httpResponse() - .map(SdkHttpResponse::statusCode)) - .orElse(-1); - - final String errorCode; - final String errorMessage; - if (execContext.exception() instanceof AwsServiceException ae) { - errorCode = ae.awsErrorDetails().errorCode(); - errorMessage = ae.awsErrorDetails().errorMessage(); - } else { - errorCode = execContext.exception().getClass().getSimpleName(); - errorMessage = execContext.exception().getMessage(); - } - var op = executionAttributes.getAttribute(OPERATION); - var s3Exception = new S3Exception(execContext.exception(), errorCode, errorMessage); - telemetryContext.close(op.method(), op.bucket(), op.key(), statusCode, s3Exception); - } - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3KoraAsyncClient.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3KoraAsyncClient.java deleted file mode 100644 index be7d0a870..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3KoraAsyncClient.java +++ /dev/null @@ -1,344 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import jakarta.annotation.Nullable; -import org.jetbrains.annotations.ApiStatus; -import reactor.adapter.JdkFlowAdapter; -import ru.tinkoff.kora.common.Context; -import ru.tinkoff.kora.s3.client.S3DeleteException; -import ru.tinkoff.kora.s3.client.S3Exception; -import ru.tinkoff.kora.s3.client.S3KoraAsyncClient; -import ru.tinkoff.kora.s3.client.S3NotFoundException; -import ru.tinkoff.kora.s3.client.model.S3Object; -import ru.tinkoff.kora.s3.client.model.*; -import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientTelemetry; -import software.amazon.awssdk.awscore.exception.AwsServiceException; -import software.amazon.awssdk.core.async.AsyncRequestBody; -import software.amazon.awssdk.core.async.AsyncResponseTransformer; -import software.amazon.awssdk.services.s3.S3AsyncClient; -import software.amazon.awssdk.services.s3.internal.multipart.MultipartS3AsyncClient; -import software.amazon.awssdk.services.s3.model.*; -import software.amazon.awssdk.services.s3.multipart.MultipartConfiguration; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ExecutorService; -import java.util.function.Function; -import java.util.function.Supplier; - -@ApiStatus.Experimental -public class AwsS3KoraAsyncClient implements S3KoraAsyncClient { - - private final S3AsyncClient asyncClient; - private final S3AsyncClient multipartAsyncClient; - private final ExecutorService awsExecutor; - private final S3KoraClientTelemetry telemetry; - private final AwsS3ClientConfig awsS3ClientConfig; - - public AwsS3KoraAsyncClient(S3AsyncClient asyncClient, - ExecutorService awsExecutor, - S3KoraClientTelemetry telemetry, - AwsS3ClientConfig awsS3ClientConfig) { - this.asyncClient = asyncClient; - this.awsExecutor = awsExecutor; - this.telemetry = telemetry; - - this.awsS3ClientConfig = awsS3ClientConfig; - this.multipartAsyncClient = MultipartS3AsyncClient.create(asyncClient, - MultipartConfiguration.builder() - .thresholdInBytes(awsS3ClientConfig.upload().partSize().toBytes()) - .apiCallBufferSizeInBytes(awsS3ClientConfig.upload().bufferSize().toBytes()) - .minimumPartSizeInBytes(awsS3ClientConfig.upload().partSize().toBytes()) - .build()); - } - - private CompletionStage getInternal(String bucket, String key) { - var request = GetObjectRequest.builder() - .bucket(bucket) - .key(key) - .build(); - - return asyncClient.getObject(request, AsyncResponseTransformer.toPublisher()) - .thenApply(r -> new AwsS3Object(request.key(), r)); - } - - @Override - public CompletionStage get(String bucket, String key) { - return wrapWithTelemetry(getInternal(bucket, key), - () -> telemetry.get("GetObject", bucket, key, null)); - } - - private CompletionStage getMetaInternal(String bucket, String key) { - var request = GetObjectAttributesRequest.builder() - .bucket(bucket) - .key(key) - .objectAttributes(ObjectAttributes.OBJECT_SIZE) - .build(); - - return asyncClient.getObjectAttributes(request) - .thenApply(r -> new AwsS3ObjectMeta(key, r)); - } - - @Override - public CompletionStage getMeta(String bucket, String key) { - return wrapWithTelemetry(getMetaInternal(bucket, key), - () -> telemetry.get("GetObjectMeta", bucket, key, null)); - } - - @Override - public CompletionStage> get(String bucket, Collection keys) { - var futures = keys.stream() - .map(k -> getInternal(bucket, k).toCompletableFuture()) - .toArray(CompletableFuture[]::new); - - var operation = CompletableFuture.allOf(futures) - .thenApply(_v -> Arrays.stream(futures) - .map(f -> ((S3Object) f.join())) - .toList()) - .exceptionallyCompose(AwsS3KoraAsyncClient::handleExceptionStage); - - return wrapWithTelemetry(operation, - () -> telemetry.get("GetObjects", bucket, null, null)); - } - - @Override - public CompletionStage> getMeta(String bucket, Collection keys) { - var futures = keys.stream() - .map(k -> getMetaInternal(bucket, k).toCompletableFuture()) - .toArray(CompletableFuture[]::new); - - var operation = CompletableFuture.allOf(futures) - .thenApply(_v -> Arrays.stream(futures) - .map(f -> ((S3ObjectMeta) f.join())) - .toList()) - .exceptionallyCompose(AwsS3KoraAsyncClient::handleExceptionStage); - - return wrapWithTelemetry(operation, - () -> telemetry.get("GetObjectMetas", bucket, null, null)); - } - - @Override - public CompletionStage list(String bucket, String prefix, @Nullable String delimiter, int limit) { - return wrapWithTelemetry(fork -> listInternal(bucket, prefix, delimiter, limit, fork), - () -> telemetry.get("ListObjects", bucket, prefix, null)); - } - - private CompletionStage listInternal(String bucket, String prefix, @Nullable String delimiter, int limit, Context context) { - return listMetaInternal(bucket, prefix, delimiter, limit) - .thenCompose(metaList -> { - try { - context.inject(); - - var futures = metaList.metas().stream() - .map(meta -> getInternal(bucket, meta.key()).toCompletableFuture()) - .toArray(CompletableFuture[]::new); - - return CompletableFuture.allOf(futures) - .thenApply(_v -> { - final List objects = new ArrayList<>(futures.length); - for (var future : futures) { - objects.add(((S3Object) future.join())); - } - - return new AwsS3ObjectList(((AwsS3ObjectMetaList) metaList).response(), objects); - }); - } finally { - Context.clear(); - } - }); - } - - private CompletionStage listMetaInternal(String bucket, String prefix, @Nullable String delimiter, int limit) { - var request = ListObjectsV2Request.builder() - .bucket(bucket) - .prefix(prefix) - .maxKeys(limit) - .delimiter(delimiter) - .build(); - - return asyncClient.listObjectsV2(request) - .thenApply(response -> new AwsS3ObjectMetaList(response)); - } - - @Override - public CompletionStage listMeta(String bucket, String prefix, @Nullable String delimiter, int limit) { - return wrapWithTelemetry(listMetaInternal(bucket, prefix, delimiter, limit), - () -> telemetry.get("ListObjectMetas", bucket, prefix, null)); - } - - @Override - public CompletionStage> list(String bucket, Collection prefixes, @Nullable String delimiter, int limitPerPrefix) { - return wrapWithTelemetry(fork -> { - var futures = prefixes.stream() - .map(p -> listInternal(bucket, p, delimiter, limitPerPrefix, fork).toCompletableFuture()) - .toArray(CompletableFuture[]::new); - - return CompletableFuture.allOf(futures) - .thenApply(_v -> Arrays.stream(futures) - .map(f -> ((S3ObjectList) f.join())) - .toList()); - }, () -> telemetry.get("ListMultiObjects", bucket, null, null)); - } - - @Override - public CompletionStage> listMeta(String bucket, Collection prefixes, @Nullable String delimiter, int limitPerPrefix) { - return wrapWithTelemetry(fork -> { - var futures = prefixes.stream() - .map(p -> listMetaInternal(bucket, p, delimiter, limitPerPrefix).toCompletableFuture()) - .toArray(CompletableFuture[]::new); - - return CompletableFuture.allOf(futures) - .thenApply(_v -> Arrays.stream(futures) - .map(f -> ((S3ObjectMetaList) f.join())) - .toList()); - }, () -> telemetry.get("ListMultiObjectMetas", bucket, null, null)); - } - - @Override - public CompletionStage put(String bucket, String key, S3Body body) { - var requestBuilder = PutObjectRequest.builder() - .bucket(bucket) - .key(key) - .contentType(body.type()) - .contentEncoding(body.encoding()); - - if (body.size() > 0) { - requestBuilder.contentLength(body.size()); - } - - var request = requestBuilder.build(); - - var ctx = Context.current(); - try { - var fork = ctx.fork(); - fork.inject(); - - var size = body.size() > 0 ? body.size() : null; - var context = telemetry.get("PutObject", bucket, key, size); - - final CompletionStage operation; - if (body instanceof ByteS3Body bb) { - operation = asyncClient.putObject(request, AsyncRequestBody.fromBytes(bb.bytes())) - .thenApply(AwsS3ObjectUpload::new); - } else if (body instanceof PublisherS3Body) { - operation = asyncClient.putObject(request, AsyncRequestBody.fromPublisher(JdkFlowAdapter.flowPublisherToFlux(body.asPublisher()))) - .thenApply(AwsS3ObjectUpload::new); - } else if (body.size() > 0 && body.size() <= awsS3ClientConfig.upload().partSize().toBytes()) { - operation = asyncClient.putObject(request, AsyncRequestBody.fromInputStream(body.asInputStream(), body.size(), awsExecutor)) - .thenApply(AwsS3ObjectUpload::new); - } else { - operation = multipartAsyncClient.putObject(request, AsyncRequestBody.fromInputStream(body.asInputStream(), size, awsExecutor)) - .thenApply(AwsS3ObjectUpload::new); - } - - return operation - .exceptionallyCompose(AwsS3KoraAsyncClient::handleExceptionStage) - .whenComplete((r, e) -> { - if (e != null) { - context.close(handleException(e)); - } else { - context.close(); - } - }); - } finally { - ctx.inject(); - } - } - - @Override - public CompletionStage delete(String bucket, String key) { - var request = DeleteObjectRequest.builder() - .bucket(bucket) - .key(key) - .build(); - - var operation = asyncClient.deleteObject(request) - .thenAccept(r -> {}); - - return wrapWithTelemetry(operation, - () -> telemetry.get("DeleteObject", bucket, key, null)); - } - - @Override - public CompletionStage delete(String bucket, Collection keys) { - var request = DeleteObjectsRequest.builder() - .bucket(bucket) - .delete(Delete.builder() - .objects(keys.stream() - .map(k -> ObjectIdentifier.builder() - .key(k) - .build()) - .toList()) - .build()) - .build(); - - CompletableFuture operation = asyncClient.deleteObjects(request) - .thenApply(response -> { - if (response.hasErrors()) { - var errors = response.errors().stream() - .map(e -> new S3DeleteException.Error(e.key(), bucket, e.code(), e.message())) - .toList(); - - throw new S3DeleteException(errors); - } - - return null; - }); - - return wrapWithTelemetry(operation, - () -> telemetry.get("DeleteObjects", bucket, null, null)); - } - - private static CompletionStage wrapWithTelemetry(CompletionStage operationSupplier, - Supplier contextSupplier) { - return wrapWithTelemetry(context -> operationSupplier, contextSupplier); - } - - private static CompletionStage wrapWithTelemetry(Function> operationSupplier, - Supplier contextSupplier) { - var ctx = Context.current(); - try { - var fork = ctx.fork(); - fork.inject(); - - var context = contextSupplier.get(); - return operationSupplier.apply(fork) - .exceptionallyCompose(AwsS3KoraAsyncClient::handleExceptionStage) - .whenComplete((r, e) -> { - if (e != null) { - context.close(handleException(e)); - } else { - context.close(); - } - }); - } finally { - ctx.inject(); - } - } - - private static CompletionStage handleExceptionStage(Throwable e) { - return CompletableFuture.failedFuture(handleException(e)); - } - - private static S3Exception handleException(Throwable e) { - if (e instanceof CompletionException ce) { - e = ce.getCause(); - } - - if (e instanceof S3Exception se) { - return se; - } else if (e instanceof NoSuchKeyException ke) { - return S3NotFoundException.ofNoSuchKey(e, ke.awsErrorDetails().errorMessage()); - } else if (e instanceof NoSuchBucketException be) { - return S3NotFoundException.ofNoSuchBucket(e, be.awsErrorDetails().errorMessage()); - } else if (e instanceof AwsServiceException ae) { - return new S3Exception(e, ae.awsErrorDetails().errorCode(), ae.awsErrorDetails().errorMessage()); - } else { - return new S3Exception(e, e.getClass().getSimpleName(), e.getMessage()); - } - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3KoraClient.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3KoraClient.java deleted file mode 100644 index b9d8db6f6..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3KoraClient.java +++ /dev/null @@ -1,317 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import jakarta.annotation.Nullable; -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.common.Context; -import ru.tinkoff.kora.s3.client.S3Exception; -import ru.tinkoff.kora.s3.client.*; -import ru.tinkoff.kora.s3.client.model.S3Object; -import ru.tinkoff.kora.s3.client.model.*; -import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientTelemetry; -import software.amazon.awssdk.awscore.exception.AwsServiceException; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.*; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CompletionException; -import java.util.function.Supplier; - -@ApiStatus.Experimental -public class AwsS3KoraClient implements S3KoraClient { - - private final S3Client syncClient; - private final S3KoraAsyncClient asyncClient; - private final S3KoraClientTelemetry telemetry; - private final AwsS3ClientConfig awsS3ClientConfig; - - public AwsS3KoraClient(S3Client syncClient, - S3KoraAsyncClient asyncClient, - S3KoraClientTelemetry telemetry, - AwsS3ClientConfig awsS3ClientConfig) { - this.syncClient = syncClient; - this.telemetry = telemetry; - this.awsS3ClientConfig = awsS3ClientConfig; - this.asyncClient = asyncClient; - } - - @Override - public S3Object get(String bucket, String key) throws S3NotFoundException { - return wrapWithTelemetry(() -> getInternal(bucket, key), - () -> telemetry.get("GetObject", bucket, key, null)); - } - - private S3Object getInternal(String bucket, String key) throws S3NotFoundException { - var request = GetObjectRequest.builder() - .bucket(bucket) - .key(key) - .build(); - - var response = syncClient.getObject(request); - return new AwsS3Object(request.key(), response); - } - - @Override - public S3ObjectMeta getMeta(String bucket, String key) throws S3NotFoundException { - return wrapWithTelemetry(() -> getMetaInternal(bucket, key), - () -> telemetry.get("GetObjectMeta", bucket, key, null)); - } - - private S3ObjectMeta getMetaInternal(String bucket, String key) throws S3NotFoundException { - var request = GetObjectAttributesRequest.builder() - .bucket(bucket) - .key(key) - .objectAttributes(ObjectAttributes.OBJECT_SIZE) - .build(); - - var response = syncClient.getObjectAttributes(request); - return new AwsS3ObjectMeta(key, response); - } - - @Override - public List get(String bucket, Collection keys) { - return wrapWithTelemetry(() -> { - final List objects = new ArrayList<>(keys.size()); - for (String key : keys) { - try { - S3Object object = getInternal(bucket, key); - objects.add(object); - } catch (S3NotFoundException e) { - // do nothing - } - } - return objects; - }, () -> telemetry.get("GetObjects", bucket, null, null)); - } - - @Override - public List getMeta(String bucket, Collection keys) { - return wrapWithTelemetry(() -> { - final List metas = new ArrayList<>(keys.size()); - for (String key : keys) { - try { - S3ObjectMeta meta = getMeta(bucket, key); - metas.add(meta); - } catch (S3NotFoundException e) { - // do nothing - } - } - return metas; - }, () -> telemetry.get("GetObjectMetas", bucket, null, null)); - } - - @Override - public S3ObjectList list(String bucket, @Nullable String prefix, @Nullable String delimiter, int limit) { - return wrapWithTelemetry(() -> listInternal(bucket, prefix, delimiter, limit), - () -> telemetry.get("ListObjects", bucket, prefix, null)); - } - - private S3ObjectList listInternal(String bucket, @Nullable String prefix, @Nullable String delimiter, int limit) { - var metaList = listMetaInternal(bucket, prefix, delimiter, limit); - - final List objects = new ArrayList<>(metaList.metas().size()); - for (S3ObjectMeta meta : metaList.metas()) { - S3Object object = getInternal(bucket, meta.key()); - objects.add(object); - } - - return new AwsS3ObjectList(((AwsS3ObjectMetaList) metaList).response(), objects); - } - - @Override - public S3ObjectMetaList listMeta(String bucket, @Nullable String prefix, @Nullable String delimiter, int limit) { - return wrapWithTelemetry(() -> listMetaInternal(bucket, prefix, delimiter, limit), - () -> telemetry.get("ListObjectMetas", bucket, prefix, null)); - } - - private S3ObjectMetaList listMetaInternal(String bucket, @Nullable String prefix, @Nullable String delimiter, int limit) { - var request = ListObjectsV2Request.builder() - .bucket(bucket) - .maxKeys(limit) - .prefix(prefix) - .delimiter(delimiter) - .build(); - - var response = syncClient.listObjectsV2(request); - return new AwsS3ObjectMetaList(response); - } - - @Override - public List list(String bucket, Collection prefixes, @Nullable String delimiter, int limitPerPrefix) { - return wrapWithTelemetry(() -> { - final List lists = new ArrayList<>(prefixes.size()); - for (String prefix : prefixes) { - S3ObjectList list = listInternal(bucket, prefix, delimiter, limitPerPrefix); - lists.add(list); - } - return lists; - }, () -> telemetry.get("ListMultiObjects", bucket, null, null)); - } - - @Override - public List listMeta(String bucket, Collection prefixes, @Nullable String delimiter, int limitPerPrefix) { - return wrapWithTelemetry(() -> { - final List lists = new ArrayList<>(prefixes.size()); - for (String prefix : prefixes) { - var list = listMeta(bucket, prefix, delimiter, limitPerPrefix); - lists.add(list); - } - return lists; - }, () -> telemetry.get("ListMultiObjectMetas", bucket, null, null)); - } - - @Override - public S3ObjectUpload put(String bucket, String key, S3Body body) { - if (body instanceof PublisherS3Body || body.size() < 0 || body.size() > awsS3ClientConfig.upload().partSize().toBytes()) { - try { - return asyncClient.put(bucket, key, body).toCompletableFuture().join(); - } catch (Exception e) { - throw handleException(e); - } - } - - var requestBuilder = PutObjectRequest.builder() - .bucket(bucket) - .key(key) - .contentType(body.type()) - .contentEncoding(body.encoding()); - - if (body.size() > 0) { - requestBuilder.contentLength(body.size()); - } - - var request = requestBuilder.build(); - - var ctx = Context.current(); - try { - var fork = ctx.fork(); - fork.inject(); - - var context = telemetry.get("PutObject", bucket, key, body.size() > 0 ? body.size() : null); - try { - if (body instanceof ByteS3Body bb) { - final PutObjectResponse response = syncClient.putObject(request, RequestBody.fromBytes(bb.bytes())); - context.close(); - return new AwsS3ObjectUpload(response); - } else { - final PutObjectResponse response = syncClient.putObject(request, RequestBody.fromContentProvider(body::asInputStream, body.size(), body.type())); - context.close(); - return new AwsS3ObjectUpload(response); - } - } catch (Exception e) { - S3Exception ex = handleException(e); - context.close(ex); - throw ex; - } - } finally { - ctx.inject(); - } - } - - @Override - public void delete(String bucket, String key) { - var request = DeleteObjectRequest.builder() - .bucket(bucket) - .key(key) - .build(); - - var ctx = Context.current(); - try { - var fork = ctx.fork(); - fork.inject(); - - var context = telemetry.get("DeleteObject", bucket, key, null); - try { - syncClient.deleteObject(request); - context.close(); - } catch (Exception e) { - S3Exception ex = handleException(e); - context.close(ex); - throw ex; - } - } finally { - ctx.inject(); - } - } - - @Override - public void delete(String bucket, Collection keys) { - var request = DeleteObjectsRequest.builder() - .bucket(bucket) - .delete(Delete.builder() - .objects(keys.stream() - .map(k -> ObjectIdentifier.builder() - .key(k) - .build()) - .toList()) - .build()) - .build(); - - var ctx = Context.current(); - try { - var fork = ctx.fork(); - fork.inject(); - - var context = telemetry.get("DeleteObjects", bucket, null, null); - try { - var response = syncClient.deleteObjects(request); - if (response.hasErrors()) { - var errors = response.errors().stream() - .map(e -> new S3DeleteException.Error(e.key(), bucket, e.code(), e.message())) - .toList(); - - throw new S3DeleteException(errors); - } - context.close(); - } catch (Exception e) { - S3Exception ex = handleException(e); - context.close(ex); - throw ex; - } - } finally { - ctx.inject(); - } - } - - private static T wrapWithTelemetry(Supplier operationSupplier, - Supplier contextSupplier) { - var ctx = Context.current(); - try { - var fork = ctx.fork(); - fork.inject(); - - var context = contextSupplier.get(); - try { - T value = operationSupplier.get(); - context.close(); - return value; - } catch (Exception e) { - S3Exception ex = handleException(e); - context.close(ex); - throw ex; - } - } finally { - ctx.inject(); - } - } - - private static S3Exception handleException(Throwable e) { - if (e instanceof CompletionException ce) { - e = ce.getCause(); - } - - if (e instanceof S3Exception se) { - return se; - } else if (e instanceof NoSuchKeyException ke) { - return S3NotFoundException.ofNoSuchKey(e, ke.awsErrorDetails().errorMessage()); - } else if (e instanceof NoSuchBucketException be) { - return S3NotFoundException.ofNoSuchBucket(e, be.awsErrorDetails().errorMessage()); - } else if (e instanceof AwsServiceException ae) { - return new S3Exception(e, ae.awsErrorDetails().errorCode(), ae.awsErrorDetails().errorMessage()); - } else { - return new S3Exception(e, e.getClass().getSimpleName(), e.getMessage()); - } - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3Object.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3Object.java deleted file mode 100644 index 0042daec1..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3Object.java +++ /dev/null @@ -1,81 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.ApiStatus.Internal; -import reactor.adapter.JdkFlowAdapter; -import ru.tinkoff.kora.s3.client.model.S3Body; -import ru.tinkoff.kora.s3.client.model.S3Object; -import ru.tinkoff.kora.s3.client.model.S3ObjectMeta; -import software.amazon.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.core.async.ResponsePublisher; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; - -import java.time.Instant; -import java.util.Objects; - -@ApiStatus.Experimental -@Internal -public final class AwsS3Object implements S3Object, S3ObjectMeta { - - private final S3Body body; - private final S3ObjectMeta meta; - private final GetObjectResponse response; - - public AwsS3Object(String key, ResponseInputStream response) { - GetObjectResponse res = response.response(); - long size = res.contentLength() == null ? -1 : res.contentLength(); - this.body = new AwsS3BodySync(res.contentEncoding(), res.contentType(), size, response); - this.meta = new AwsS3ObjectMeta(key, res); - this.response = res; - } - - public AwsS3Object(String key, ResponsePublisher response) { - GetObjectResponse res = response.response(); - long size = res.contentLength() == null ? -1 : res.contentLength(); - this.body = new AwsS3BodyAsync(res.contentEncoding(), res.contentType(), size, JdkFlowAdapter.publisherToFlowPublisher(response)); - this.meta = new AwsS3ObjectMeta(key, res); - this.response = res; - } - - @Override - public String key() { - return meta.key(); - } - - @Override - public Instant modified() { - return meta.modified(); - } - - @Override - public long size() { - return meta.size(); - } - - @Override - public S3Body body() { - return body; - } - - public GetObjectResponse response() { - return response; - } - - @Override - public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; - AwsS3Object s3Object = (AwsS3Object) object; - return Objects.equals(meta, s3Object.meta); - } - - @Override - public int hashCode() { - return Objects.hash(meta); - } - - @Override - public String toString() { - return body.toString(); - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectList.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectList.java deleted file mode 100644 index 3d1301317..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectList.java +++ /dev/null @@ -1,42 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.s3.client.model.S3Object; -import ru.tinkoff.kora.s3.client.model.S3ObjectList; -import ru.tinkoff.kora.s3.client.model.S3ObjectMeta; -import ru.tinkoff.kora.s3.client.model.S3ObjectMetaList; -import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; - -import java.util.List; - -@ApiStatus.Experimental -final class AwsS3ObjectList implements S3ObjectList { - - private final S3ObjectMetaList metaList; - private final List objects; - - public AwsS3ObjectList(ListObjectsV2Response response, List objects) { - this.objects = objects; - this.metaList = new AwsS3ObjectMetaList(response); - } - - @Override - public List objects() { - return objects; - } - - @Override - public String prefix() { - return metaList.prefix(); - } - - @Override - public List metas() { - return (List) ((List) objects); - } - - @Override - public String toString() { - return objects.toString(); - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectMeta.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectMeta.java deleted file mode 100644 index d95c54b59..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectMeta.java +++ /dev/null @@ -1,72 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.s3.client.model.S3ObjectMeta; -import software.amazon.awssdk.services.s3.model.GetObjectAttributesResponse; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.s3.model.S3Object; - -import java.time.Instant; -import java.util.Objects; - -@ApiStatus.Experimental -final class AwsS3ObjectMeta implements S3ObjectMeta { - - private final String key; - private final Instant modified; - private final long size; - - public AwsS3ObjectMeta(String key, GetObjectResponse response) { - this.key = key; - this.modified = response.lastModified(); - this.size = response.contentLength(); - } - - public AwsS3ObjectMeta(String key, GetObjectAttributesResponse response) { - this.key = key; - this.modified = response.lastModified(); - this.size = response.objectSize(); - } - - public AwsS3ObjectMeta(S3Object object) { - this.key = object.key(); - this.modified = object.lastModified(); - this.size = object.size(); - } - - @Override - public String key() { - return key; - } - - @Override - public Instant modified() { - return modified; - } - - @Override - public long size() { - return size; - } - - @Override - public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; - AwsS3ObjectMeta meta = (AwsS3ObjectMeta) object; - return size == meta.size && Objects.equals(key, meta.key); - } - - @Override - public int hashCode() { - return Objects.hash(key, size); - } - - @Override - public String toString() { - return "AwsS3ObjectMeta{key=" + key + - ", size=" + size + - ", modified=" + modified + - '}'; - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectMetaList.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectMetaList.java deleted file mode 100644 index b42a43f7c..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectMetaList.java +++ /dev/null @@ -1,43 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.ApiStatus.Internal; -import ru.tinkoff.kora.s3.client.model.S3ObjectMeta; -import ru.tinkoff.kora.s3.client.model.S3ObjectMetaList; -import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; - -import java.util.List; - -@ApiStatus.Experimental -@Internal -public final class AwsS3ObjectMetaList implements S3ObjectMetaList { - - private final ListObjectsV2Response response; - private final List metas; - - public AwsS3ObjectMetaList(ListObjectsV2Response response) { - this.response = response; - this.metas = response.contents().stream() - .map(AwsS3ObjectMeta::new) - .toList(); - } - - @Override - public String prefix() { - return response.prefix(); - } - - @Override - public List metas() { - return metas; - } - - public ListObjectsV2Response response() { - return response; - } - - @Override - public String toString() { - return metas.toString(); - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectUpload.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectUpload.java deleted file mode 100644 index 49bc9ff20..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/AwsS3ObjectUpload.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.s3.client.model.S3ObjectUpload; -import software.amazon.awssdk.services.s3.model.PutObjectResponse; - -@ApiStatus.Experimental -public record AwsS3ObjectUpload(String versionId, PutObjectResponse response) implements S3ObjectUpload { - - public AwsS3ObjectUpload(PutObjectResponse response) { - this(response.versionId(), response); - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/KoraAwsSdkHttpClient.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/KoraAwsSdkHttpClient.java deleted file mode 100644 index 7cb8896e1..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/KoraAwsSdkHttpClient.java +++ /dev/null @@ -1,195 +0,0 @@ -package ru.tinkoff.kora.s3.client.aws; - -import org.jetbrains.annotations.ApiStatus; -import reactor.adapter.JdkFlowAdapter; -import ru.tinkoff.kora.http.client.common.HttpClient; -import ru.tinkoff.kora.http.client.common.request.HttpClientRequest; -import ru.tinkoff.kora.http.client.common.request.HttpClientRequestBuilder; -import ru.tinkoff.kora.http.client.common.response.HttpClientResponse; -import ru.tinkoff.kora.http.common.body.HttpBodyInput; -import ru.tinkoff.kora.http.common.body.HttpBodyOutput; -import software.amazon.awssdk.http.*; -import software.amazon.awssdk.http.async.AsyncExecuteRequest; -import software.amazon.awssdk.http.async.SdkAsyncHttpClient; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Flow; - -@ApiStatus.Experimental -public final class KoraAwsSdkHttpClient implements SdkHttpClient, SdkAsyncHttpClient { - - private final HttpClient httpClient; - private final AwsS3ClientConfig clientConfig; - - public KoraAwsSdkHttpClient(HttpClient httpClient, AwsS3ClientConfig clientConfig) { - this.httpClient = httpClient; - this.clientConfig = clientConfig; - } - - @Override - public String clientName() { - return "Kora"; - } - - @Override - public ExecutableHttpRequest prepareRequest(HttpExecuteRequest httpExecuteRequest) { - return new ExecutableHttpRequest() { - - @Override - public HttpExecuteResponse call() { - final HttpClientRequest request = asKoraRequest(httpExecuteRequest); - final HttpClientResponse response = httpClient.execute(request).toCompletableFuture().join(); - return asAwsResponse(response); - } - - @Override - public void abort() { - // do nothing - } - }; - } - - @Override - public CompletableFuture execute(AsyncExecuteRequest asyncExecuteRequest) { - final HttpClientRequest request = asKoraRequest(asyncExecuteRequest); - return httpClient.execute(request) - .thenAccept(response -> { - final SdkHttpResponse sdkHttpResponse = asSdkResponse(response); - asyncExecuteRequest.responseHandler().onHeaders(sdkHttpResponse); - asyncExecuteRequest.responseHandler().onStream(JdkFlowAdapter.flowPublisherToFlux(response.body())); - }) - .exceptionally(e -> { - asyncExecuteRequest.responseHandler().onError(e); - return null; - }) - .toCompletableFuture(); - } - - private HttpClientRequest asKoraRequest(HttpExecuteRequest httpExecuteRequest) { - final SdkHttpRequest sdkHttpRequest = httpExecuteRequest.httpRequest(); - final HttpClientRequestBuilder builder = getBaseBuilder(sdkHttpRequest.getUri(), sdkHttpRequest.method().name(), sdkHttpRequest.rawQueryParameters(), sdkHttpRequest.headers()); - - httpExecuteRequest.contentStreamProvider().ifPresent(provider -> { - String contentType = sdkHttpRequest.firstMatchingHeader("Content-Type").orElse("application/octet-stream"); - String contentLength = sdkHttpRequest.firstMatchingHeader("Content-Length").orElse(null); - if (contentLength == null) { - builder.body(HttpBodyOutput.of(contentType, provider.newStream())); - } else { - builder.body(HttpBodyOutput.of(contentType, Long.parseLong(contentLength), provider.newStream())); - } - }); - - return builder - .requestTimeout(clientConfig.requestTimeout()) - .build(); - } - - private HttpClientRequest asKoraRequest(AsyncExecuteRequest asyncExecuteRequest) { - final SdkHttpRequest sdkHttpRequest = asyncExecuteRequest.request(); - final HttpClientRequestBuilder builder = getBaseBuilder(sdkHttpRequest.getUri(), sdkHttpRequest.method().name(), sdkHttpRequest.rawQueryParameters(), sdkHttpRequest.headers()); - - Flow.Publisher bodyFlow = JdkFlowAdapter.publisherToFlowPublisher(asyncExecuteRequest.requestContentPublisher()); - String contentType = sdkHttpRequest.firstMatchingHeader("Content-Type").orElse("application/octet-stream"); - String contentLength = sdkHttpRequest.firstMatchingHeader("Content-Length").orElse(null); - if (contentLength == null) { - builder.body(HttpBodyOutput.of(contentType, bodyFlow)); - } else { - builder.body(HttpBodyOutput.of(contentType, Long.parseLong(contentLength), bodyFlow)); - } - - return builder - .requestTimeout(clientConfig.requestTimeout()) - .build(); - } - - private static HttpClientRequestBuilder getBaseBuilder(URI sdkUri, - String method, - Map> rawQueryParameters, - Map> headers) { - try { - final URI uri = new URI(sdkUri.getScheme(), - sdkUri.getAuthority(), - sdkUri.getPath(), - null, // Ignore the query part of the input url - sdkUri.getFragment()); - - final HttpClientRequestBuilder builder = HttpClientRequest.of(method, uri.toString()); - rawQueryParameters.forEach((k, v) -> { - if (v == null || v.isEmpty() || v.get(0) == null) { - builder.queryParam(k); - } else { - builder.queryParam(k, v); - } - }); - - headers.forEach((k, v) -> { - if (!"host".equalsIgnoreCase(k) && !"expect".equalsIgnoreCase(k)) { - builder.header(k, v); - } - }); - - return builder; - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - } - - private static HttpExecuteResponse asAwsResponse(HttpClientResponse koraHttpResponse) { - final SdkHttpFullResponse.Builder sdkResponseBuilder = SdkHttpResponse.builder(); - final Map> responseHeaders = new HashMap<>(); - koraHttpResponse.headers().forEach(e -> responseHeaders.put(e.getKey(), e.getValue())); - sdkResponseBuilder.headers(responseHeaders); - sdkResponseBuilder.statusCode(koraHttpResponse.code()); - sdkResponseBuilder.statusText(String.valueOf(koraHttpResponse.code())); - - AbortableInputStream bodyStream = asSdkResponseStream(koraHttpResponse); - sdkResponseBuilder.content(bodyStream); - - final SdkHttpFullResponse sdkHttpResponse = sdkResponseBuilder.build(); - return HttpExecuteResponse.builder() - .response(sdkHttpResponse) - .responseBody(bodyStream) - .build(); - } - - private static SdkHttpFullResponse asSdkResponse(HttpClientResponse koraHttpResponse) { - final SdkHttpFullResponse.Builder sdkResponseBuilder = SdkHttpResponse.builder(); - final Map> responseHeaders = new HashMap<>(); - koraHttpResponse.headers().forEach(e -> responseHeaders.put(e.getKey(), e.getValue())); - sdkResponseBuilder.headers(responseHeaders); - sdkResponseBuilder.statusCode(koraHttpResponse.code()); - sdkResponseBuilder.statusText(String.valueOf(koraHttpResponse.code())); - - return sdkResponseBuilder.build(); - } - - private static AbortableInputStream asSdkResponseStream(HttpClientResponse koraHttpResponse) { - final HttpBodyInput body = koraHttpResponse.body(); - final InputStream bodyIS = body.asInputStream(); - final InputStream bodyAsInputStream = bodyIS != null - ? bodyIS - : new ByteArrayInputStream(body.asArrayStage().toCompletableFuture().join()); - - return AbortableInputStream.create(bodyAsInputStream, () -> { - try { - bodyAsInputStream.close(); - } catch (IOException e) { - // ignore - } - }); - } - - @Override - public void close() { - // do nothing - } -} diff --git a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/package-info.java b/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/package-info.java deleted file mode 100644 index 7dc84a84c..000000000 --- a/experimental/s3-client-aws/src/main/java/ru/tinkoff/kora/s3/client/aws/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -@Experimental -package ru.tinkoff.kora.s3.client.aws; - -import org.jetbrains.annotations.ApiStatus.Experimental; diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3ClientConfig.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3ClientConfig.java deleted file mode 100644 index 51d1ea499..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3ClientConfig.java +++ /dev/null @@ -1,12 +0,0 @@ -package ru.tinkoff.kora.s3.client; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.config.common.annotation.ConfigValueExtractor; - -@ApiStatus.Experimental -@ConfigValueExtractor -public interface S3ClientConfig { - - String bucket(); -} - diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3ClientModule.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3ClientModule.java deleted file mode 100644 index e1be8e42f..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3ClientModule.java +++ /dev/null @@ -1,41 +0,0 @@ -package ru.tinkoff.kora.s3.client; - -import jakarta.annotation.Nullable; -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.common.DefaultComponent; -import ru.tinkoff.kora.config.common.Config; -import ru.tinkoff.kora.config.common.extractor.ConfigValueExtractor; -import ru.tinkoff.kora.s3.client.telemetry.*; - -@ApiStatus.Experimental -public interface S3ClientModule { - - default S3Config s3Config(Config config, ConfigValueExtractor extractor) { - var value = config.get("s3client"); - return extractor.extract(value); - } - - @DefaultComponent - default S3ClientLoggerFactory s3ClientLoggerFactory() { - return new DefaultS3ClientLoggerFactory(); - } - - @DefaultComponent - default S3ClientTelemetryFactory s3ClientTelemetryFactory(@Nullable S3ClientLoggerFactory loggerFactory, - @Nullable S3ClientTracerFactory tracingFactory, - @Nullable S3ClientMetricsFactory metricsFactory) { - return new DefaultS3ClientTelemetryFactory(loggerFactory, tracingFactory, metricsFactory); - } - - @DefaultComponent - default S3KoraClientLoggerFactory s3KoraClientLoggerFactory() { - return new DefaultS3KoraClientLoggerFactory(); - } - - @DefaultComponent - default S3KoraClientTelemetryFactory s3KoraClientTelemetryFactory(@Nullable S3KoraClientLoggerFactory loggerFactory, - @Nullable S3KoraClientTracerFactory tracingFactory, - @Nullable S3KoraClientMetricsFactory metricsFactory) { - return new DefaultS3KoraClientTelemetryFactory(loggerFactory, tracingFactory, metricsFactory); - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3Config.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3Config.java deleted file mode 100644 index b9fd62d61..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3Config.java +++ /dev/null @@ -1,23 +0,0 @@ -package ru.tinkoff.kora.s3.client; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.config.common.annotation.ConfigValueExtractor; -import ru.tinkoff.kora.telemetry.common.TelemetryConfig; - -@ApiStatus.Experimental -@ConfigValueExtractor -public interface S3Config { - - String url(); - - String accessKey(); - - String secretKey(); - - default String region() { - return "aws-global"; - } - - TelemetryConfig telemetry(); -} - diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3DeleteException.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3DeleteException.java deleted file mode 100644 index 128dce402..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3DeleteException.java +++ /dev/null @@ -1,27 +0,0 @@ -package ru.tinkoff.kora.s3.client; - -import org.jetbrains.annotations.ApiStatus; - -import java.util.List; -import java.util.stream.Collectors; - -@ApiStatus.Experimental -public class S3DeleteException extends S3Exception { - - public record Error(String key, String bucket, String code, String message) {} - - private final List errors; - - public S3DeleteException(List errors) { - super(new IllegalStateException(errors.stream() - .map(Error::message) - .collect(Collectors.joining(", ", "Errors occurred while deleting objects: ", ""))), - errors.get(0).code(), - errors.get(0).message()); - this.errors = errors; - } - - public List getErrors() { - return errors; - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3Exception.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3Exception.java deleted file mode 100644 index 89162fe77..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3Exception.java +++ /dev/null @@ -1,30 +0,0 @@ -package ru.tinkoff.kora.s3.client; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Experimental -public class S3Exception extends RuntimeException { - - private final String errorCode; - private final String errorMessage; - - public S3Exception(String message, String errorCode, String errorMessage) { - super(message); - this.errorCode = errorCode; - this.errorMessage = errorMessage; - } - - public S3Exception(Throwable cause, String errorCode, String errorMessage) { - super(cause); - this.errorCode = errorCode; - this.errorMessage = errorMessage; - } - - public String getErrorCode() { - return errorCode; - } - - public String getErrorMessage() { - return errorMessage; - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3KoraAsyncClient.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3KoraAsyncClient.java deleted file mode 100644 index deaaf2048..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3KoraAsyncClient.java +++ /dev/null @@ -1,76 +0,0 @@ -package ru.tinkoff.kora.s3.client; - -import jakarta.annotation.Nullable; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Range; -import ru.tinkoff.kora.s3.client.model.*; - -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CompletionStage; - -@ApiStatus.Experimental -public interface S3KoraAsyncClient { - - CompletionStage get(String bucket, String key) throws S3NotFoundException; - - CompletionStage getMeta(String bucket, String key) throws S3NotFoundException; - - CompletionStage> get(String bucket, Collection keys) throws S3NotFoundException; - - CompletionStage> getMeta(String bucket, Collection keys) throws S3NotFoundException; - - default CompletionStage list(String bucket) throws S3NotFoundException { - return list(bucket, (String) null, null, 1000); - } - - default CompletionStage list(String bucket, - @Nullable String prefix) throws S3NotFoundException { - return list(bucket, prefix, null, 1000); - } - - CompletionStage list(String bucket, - @Nullable String prefix, - @Nullable String delimiter, - @Range(from = 1, to = 1000) int limit) throws S3NotFoundException; - - default CompletionStage listMeta(String bucket) { - return listMeta(bucket, (String) null, null, 1000); - } - - default CompletionStage listMeta(String bucket, - @Nullable String prefix) { - return listMeta(bucket, prefix, null, 1000); - } - - CompletionStage listMeta(String bucket, - @Nullable String prefix, - @Nullable String delimiter, - @Range(from = 1, to = 1000) int limit); - - default CompletionStage> list(String bucket, - Collection prefixes) throws S3NotFoundException { - return list(bucket, prefixes, null, 1000); - } - - CompletionStage> list(String bucket, - Collection prefixes, - @Nullable String delimiter, - @Range(from = 1, to = 1000) int limitPerPrefix) throws S3NotFoundException; - - default CompletionStage> listMeta(String bucket, - Collection prefixes) { - return listMeta(bucket, prefixes, null, 1000); - } - - CompletionStage> listMeta(String bucket, - Collection prefixes, - @Nullable String delimiter, - @Range(from = 1, to = 1000) int limitPerPrefix); - - CompletionStage put(String bucket, String key, S3Body body); - - CompletionStage delete(String bucket, String key); - - CompletionStage delete(String bucket, Collection keys) throws S3DeleteException; -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3KoraClient.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3KoraClient.java deleted file mode 100644 index 484e3aeef..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3KoraClient.java +++ /dev/null @@ -1,75 +0,0 @@ -package ru.tinkoff.kora.s3.client; - -import jakarta.annotation.Nullable; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Range; -import ru.tinkoff.kora.s3.client.model.*; - -import java.util.Collection; -import java.util.List; - -@ApiStatus.Experimental -public interface S3KoraClient { - - S3Object get(String bucket, String key) throws S3NotFoundException; - - S3ObjectMeta getMeta(String bucket, String key) throws S3NotFoundException; - - List get(String bucket, Collection keys) throws S3NotFoundException; - - List getMeta(String bucket, Collection keys) throws S3NotFoundException; - - default S3ObjectList list(String bucket) throws S3NotFoundException { - return list(bucket, ((String) null), null, 1000); - } - - default S3ObjectList list(String bucket, - @Nullable String prefix) throws S3NotFoundException { - return list(bucket, prefix, null, 1000); - } - - S3ObjectList list(String bucket, - @Nullable String prefix, - @Nullable String delimiter, - @Range(from = 1, to = 1000) int limit) throws S3NotFoundException; - - default S3ObjectMetaList listMeta(String bucket) throws S3NotFoundException { - return listMeta(bucket, ((String) null), null, 1000); - } - - default S3ObjectMetaList listMeta(String bucket, - @Nullable String prefix) throws S3NotFoundException { - return listMeta(bucket, prefix, null, 1000); - } - - S3ObjectMetaList listMeta(String bucket, - @Nullable String prefix, - @Nullable String delimiter, - @Range(from = 1, to = 1000) int limit) throws S3NotFoundException; - - default List list(String bucket, - Collection prefixes) throws S3NotFoundException { - return list(bucket, prefixes, null, 1000); - } - - List list(String bucket, - Collection prefixes, - @Nullable String delimiter, - @Range(from = 1, to = 1000) int limitPerPrefix) throws S3NotFoundException; - - default List listMeta(String bucket, - Collection prefixes) throws S3NotFoundException { - return listMeta(bucket, prefixes, null, 1000); - } - - List listMeta(String bucket, - Collection prefixes, - @Nullable String delimiter, - @Range(from = 1, to = 1000) int limitPerPrefix) throws S3NotFoundException; - - S3ObjectUpload put(String bucket, String key, S3Body body); - - void delete(String bucket, String key); - - void delete(String bucket, Collection keys) throws S3DeleteException; -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3NotFoundException.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3NotFoundException.java deleted file mode 100644 index 5c8da60ed..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/S3NotFoundException.java +++ /dev/null @@ -1,19 +0,0 @@ -package ru.tinkoff.kora.s3.client; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Experimental -public class S3NotFoundException extends S3Exception { - - public S3NotFoundException(Throwable cause, String code, String message) { - super(cause, code, message); - } - - public static S3NotFoundException ofNoSuchKey(Throwable cause, String message) { - return new S3NotFoundException(cause, "NoSuchKey", message); - } - - public static S3NotFoundException ofNoSuchBucket(Throwable cause, String message) { - return new S3NotFoundException(cause, "NoSuchBucket", message); - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/ByteBufferPublisher.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/ByteBufferPublisher.java deleted file mode 100644 index 552560520..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/ByteBufferPublisher.java +++ /dev/null @@ -1,51 +0,0 @@ -package ru.tinkoff.kora.s3.client.model; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.common.util.FlowUtils; - -import java.io.Closeable; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.concurrent.Flow; -import java.util.concurrent.atomic.AtomicBoolean; - -@ApiStatus.Experimental -final class ByteBufferPublisher extends AtomicBoolean implements Flow.Publisher, Closeable { - - private final Flow.Publisher publisher; - - private volatile InputStreamByteBufferSubscriber is; - - public ByteBufferPublisher(Flow.Publisher response) { - this.publisher = response; - } - - @Override - public void subscribe(Flow.Subscriber subscriber) { - if (this.compareAndSet(false, true)) { - this.publisher.subscribe(subscriber); - } else { - throw new IllegalStateException("Publishers was already subscribed"); - } - } - - public InputStream asInputStream() { - var is = this.is; - if (is == null) { - if (this.compareAndSet(false, true)) { - this.is = is = new InputStreamByteBufferSubscriber(); - this.publisher.subscribe(is); - } else { - throw new IllegalStateException("Publishers was already subscribed"); - } - } - return is; - } - - @Override - public void close() { - if (this.compareAndSet(false, true)) { - this.publisher.subscribe(FlowUtils.drain()); - } - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/ByteS3Body.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/ByteS3Body.java deleted file mode 100644 index eba0e690d..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/ByteS3Body.java +++ /dev/null @@ -1,38 +0,0 @@ -package ru.tinkoff.kora.s3.client.model; - -import org.jetbrains.annotations.ApiStatus; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.net.http.HttpRequest; -import java.nio.ByteBuffer; -import java.util.concurrent.Flow; - -@ApiStatus.Experimental -public record ByteS3Body(byte[] bytes, - long size, - String type, - String encoding) implements S3Body { - - public ByteS3Body(byte[] bytes, long size, String type, String encoding) { - this.bytes = bytes; - this.size = size; - this.type = (type == null || type.isBlank()) ? "application/octet-stream" : type; - this.encoding = encoding; - } - - @Override - public byte[] asBytes() { - return bytes; - } - - @Override - public InputStream asInputStream() { - return new ByteArrayInputStream(bytes); - } - - @Override - public Flow.Publisher asPublisher() { - return HttpRequest.BodyPublishers.ofByteArray(bytes); - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/InputStreamByteBufferSubscriber.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/InputStreamByteBufferSubscriber.java deleted file mode 100644 index c8b4121d5..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/InputStreamByteBufferSubscriber.java +++ /dev/null @@ -1,231 +0,0 @@ -package ru.tinkoff.kora.s3.client.model; - - -import org.jetbrains.annotations.ApiStatus; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.Objects; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Flow; -import java.util.concurrent.atomic.AtomicBoolean; - -@ApiStatus.Experimental -final class InputStreamByteBufferSubscriber extends InputStream implements Flow.Subscriber { - - final static int MAX_BUFFERS_IN_QUEUE = 1; // lock-step with the producer - - // An immutable ByteBuffer sentinel to mark that the last byte was received. - private static final ByteBuffer LAST_BUFFER = ByteBuffer.wrap(new byte[0]); - - // A queue of yet unprocessed ByteBuffers received from the flow API. - private final BlockingQueue buffers; - private volatile Flow.Subscription subscription; - private volatile boolean closed; - private volatile Throwable failed; - private volatile ByteBuffer currentBuffer; - private final AtomicBoolean subscribed = new AtomicBoolean(); - - public InputStreamByteBufferSubscriber() { - this(MAX_BUFFERS_IN_QUEUE); - } - - InputStreamByteBufferSubscriber(int maxBuffers) { - int capacity = (maxBuffers <= 0 ? MAX_BUFFERS_IN_QUEUE : maxBuffers); - // 1 additional slot needed for LAST_LIST added by onComplete - this.buffers = new ArrayBlockingQueue<>(capacity + 1); - } - - // Returns the current byte buffer to read from. - // If the current buffer has no remaining data, this method will take the - // next buffer from the buffers queue, possibly blocking until - // a new buffer is made available through the Flow API, or the - // end of the flow has been reached. - private ByteBuffer current() throws IOException { - while (currentBuffer == null || !currentBuffer.hasRemaining()) { - // Check whether the stream is closed or exhausted - if (closed || failed != null) { - throw new IOException("closed", failed); - } - if (currentBuffer == LAST_BUFFER) { - break; - } - - try { - currentBuffer = buffers.take(); - - // Check whether an exception was encountered upstream - if (closed || failed != null) { - throw new IOException("closed", failed); - } - // Check whether we're done. - if (currentBuffer == LAST_BUFFER) { - currentBuffer = LAST_BUFFER; - break; - } - - // Request another upstream item ( list of buffers ) - Flow.Subscription s = subscription; - if (s != null) { - s.request(1); - } - } catch (InterruptedException ex) { - // continue - } - } - - assert currentBuffer == LAST_BUFFER || currentBuffer.hasRemaining(); - return currentBuffer; - } - - @Override - public int read(byte[] bytes, int off, int len) throws IOException { - Objects.checkFromIndexSize(off, len, bytes.length); - if (len == 0) { - return 0; - } - // get the buffer to read from, possibly blocking if - // none is available - ByteBuffer buffer; - if ((buffer = current()) == LAST_BUFFER) return -1; - - // don't attempt to read more than what is available - // in the current buffer. - int read = Math.min(buffer.remaining(), len); - assert read > 0 && read <= buffer.remaining(); - - // buffer.get() will do the boundary check for us. - buffer.get(bytes, off, read); - return read; - } - - @Override - public int read() throws IOException { - ByteBuffer buffer; - if ((buffer = current()) == LAST_BUFFER) return -1; - return buffer.get() & 0xFF; - } - - @Override - public int available() throws IOException { - // best effort: returns the number of remaining bytes in - // the current buffer if any, or 1 if the current buffer - // is null or empty but the queue or current buffer list - // are not empty. Returns 0 otherwise. - if (closed) { - return 0; - } - int available = 0; - ByteBuffer current = currentBuffer; - if (current == LAST_BUFFER) { - return 0; - } - if (current != null) { - available = current.remaining(); - } - if (available != 0) { - return available; - } - if (buffers.isEmpty()) { - return 0; - } - return 1; - } - - @Override - public void onSubscribe(Flow.Subscription s) { - Objects.requireNonNull(s); - try { - if (!subscribed.compareAndSet(false, true)) { - s.cancel(); - } else { - // check whether the stream is already closed. - // if so, we should cancel the subscription - // immediately. - boolean closed; - synchronized (this) { - closed = this.closed; - if (!closed) { - this.subscription = s; - } - } - if (closed) { - s.cancel(); - return; - } - assert buffers.remainingCapacity() > 1; // should contain at least 2 - s.request(Math.max(1, buffers.remainingCapacity() - 1)); - } - } catch (Throwable t) { - failed = t; - try { - close(); - } catch (IOException x) { - // OK - } finally { - onError(t); - } - } - } - - @Override - public void onNext(ByteBuffer t) { - Objects.requireNonNull(t); - try { - if (!buffers.offer(t)) { - throw new IllegalStateException("queue is full"); - } - } catch (Throwable ex) { - failed = ex; - try { - close(); - } catch (IOException ex1) { - // OK - } finally { - onError(ex); - } - } - } - - @Override - public void onError(Throwable thrwbl) { - subscription = null; - failed = Objects.requireNonNull(thrwbl); - // The client process that reads the input stream might - // be blocked in queue.take(). - // Tries to offer LAST_LIST to the queue. If the queue is - // full we don't care if we can't insert this buffer, as - // the client can't be blocked in queue.take() in that case. - // Adding LAST_LIST to the queue is harmless, as the client - // should find failed != null before handling LAST_LIST. - buffers.offer(LAST_BUFFER); - } - - @Override - public void onComplete() { - subscription = null; - onNext(LAST_BUFFER); - } - - @Override - public void close() throws IOException { - Flow.Subscription s; - synchronized (this) { - if (closed) return; - closed = true; - s = subscription; - subscription = null; - } - // s will be null if already completed - try { - if (s != null) { - s.cancel(); - } - } finally { - buffers.offer(LAST_BUFFER); - super.close(); - } - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/InputStreamS3Body.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/InputStreamS3Body.java deleted file mode 100644 index 110a559e9..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/InputStreamS3Body.java +++ /dev/null @@ -1,32 +0,0 @@ -package ru.tinkoff.kora.s3.client.model; - -import org.jetbrains.annotations.ApiStatus; - -import java.io.InputStream; -import java.net.http.HttpRequest; -import java.nio.ByteBuffer; -import java.util.concurrent.Flow; - -@ApiStatus.Experimental -public record InputStreamS3Body(InputStream inputStream, - long size, - String type, - String encoding) implements S3Body { - - public InputStreamS3Body(InputStream inputStream, long size, String type, String encoding) { - this.inputStream = inputStream; - this.size = size; - this.type = (type == null || type.isBlank()) ? "application/octet-stream" : type; - this.encoding = encoding; - } - - @Override - public InputStream asInputStream() { - return inputStream; - } - - @Override - public Flow.Publisher asPublisher() { - return HttpRequest.BodyPublishers.ofInputStream(() -> inputStream); - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/PublisherS3Body.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/PublisherS3Body.java deleted file mode 100644 index b3ec51b40..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/PublisherS3Body.java +++ /dev/null @@ -1,33 +0,0 @@ -package ru.tinkoff.kora.s3.client.model; - -import org.jetbrains.annotations.ApiStatus; - -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.concurrent.Flow; - -@ApiStatus.Experimental -public record PublisherS3Body(Flow.Publisher publisher, - long size, - String type, - String encoding) implements S3Body { - - public PublisherS3Body(Flow.Publisher publisher, long size, String type, String encoding) { - this.publisher = publisher; - this.size = size; - this.type = (type == null || type.isBlank()) ? "application/octet-stream" : type; - this.encoding = encoding; - } - - @Override - public InputStream asInputStream() { - try (var pub = new ByteBufferPublisher(publisher)) { - return pub.asInputStream(); - } - } - - @Override - public Flow.Publisher asPublisher() { - return publisher; - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3Body.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3Body.java deleted file mode 100644 index 34bf10c11..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3Body.java +++ /dev/null @@ -1,123 +0,0 @@ -package ru.tinkoff.kora.s3.client.model; - -import org.jetbrains.annotations.ApiStatus; - -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.nio.ByteBuffer; -import java.util.concurrent.Flow; - -/** - * S3 Object value representation - */ -@ApiStatus.Experimental -public interface S3Body { - - default byte[] asBytes() { - try (var stream = asInputStream()) { - return stream.readAllBytes(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - InputStream asInputStream(); - - Flow.Publisher asPublisher(); - - long size(); - - String encoding(); - - String type(); - - static S3Body ofBytes(byte[] body) { - return new ByteS3Body(body, body.length, "application/octet-stream", null); - } - - static S3Body ofBytes(byte[] body, String type) { - return new ByteS3Body(body, body.length, type, null); - } - - static S3Body ofBytes(byte[] body, String type, String encoding) { - return new ByteS3Body(body, body.length, type, encoding); - } - - static S3Body ofBuffer(ByteBuffer body) { - return new ByteS3Body(body.array(), body.remaining(), null, null); - } - - static S3Body ofBuffer(ByteBuffer body, String type) { - return new ByteS3Body(body.array(), body.remaining(), type, null); - } - - static S3Body ofBuffer(ByteBuffer body, String type, String encoding) { - return new ByteS3Body(body.array(), body.remaining(), encoding, type); - } - - static S3Body ofInputStreamUnbound(InputStream inputStream) { - return ofInputStreamUnbound(inputStream, "application/octet-stream", null); - } - - static S3Body ofInputStreamUnbound(InputStream inputStream, String type) { - return ofInputStreamUnbound(inputStream, type, null); - } - - static S3Body ofInputStreamUnbound(InputStream inputStream, String type, String encoding) { - return new InputStreamS3Body(inputStream, -1, type, encoding); - } - - static S3Body ofInputStreamReadAll(InputStream inputStream) { - return ofInputStreamReadAll(inputStream, "application/octet-stream", null); - } - - static S3Body ofInputStreamReadAll(InputStream inputStream, String type) { - return ofInputStreamReadAll(inputStream, type, null); - } - - static S3Body ofInputStreamReadAll(InputStream inputStream, String type, String encoding) { - try (inputStream) { - byte[] bytes = inputStream.readAllBytes(); - return new ByteS3Body(bytes, bytes.length, encoding, type); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - static S3Body ofInputStream(InputStream inputStream, long size) { - return ofInputStream(inputStream, size, "application/octet-stream", null); - } - - static S3Body ofInputStream(InputStream inputStream, long size, String type) { - return ofInputStream(inputStream, size, type, null); - } - - static S3Body ofInputStream(InputStream inputStream, long size, String type, String encoding) { - return new InputStreamS3Body(inputStream, size, type, encoding); - } - - static S3Body ofPublisher(Flow.Publisher publisher) { - return ofPublisher(publisher, -1, "application/octet-stream", null); - } - - static S3Body ofPublisher(Flow.Publisher publisher, String type) { - return ofPublisher(publisher, -1, type, null); - } - - static S3Body ofPublisher(Flow.Publisher publisher, String type, String encoding) { - return new PublisherS3Body(publisher, -1, type, encoding); - } - - static S3Body ofPublisher(Flow.Publisher publisher, long size) { - return ofPublisher(publisher, size, "application/octet-stream", null); - } - - static S3Body ofPublisher(Flow.Publisher publisher, long size, String type) { - return ofPublisher(publisher, size, type, null); - } - - static S3Body ofPublisher(Flow.Publisher publisher, long size, String type, String encoding) { - return new PublisherS3Body(publisher, size, type, encoding); - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3Object.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3Object.java deleted file mode 100644 index f68a59fc2..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3Object.java +++ /dev/null @@ -1,20 +0,0 @@ -package ru.tinkoff.kora.s3.client.model; - -import org.jetbrains.annotations.ApiStatus; - -import java.time.Instant; - -/** - * S3 Object representation - */ -@ApiStatus.Experimental -public interface S3Object { - - String key(); - - Instant modified(); - - long size(); - - S3Body body(); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectList.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectList.java deleted file mode 100644 index 08a164b91..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectList.java +++ /dev/null @@ -1,16 +0,0 @@ -package ru.tinkoff.kora.s3.client.model; - -import org.jetbrains.annotations.ApiStatus; - -import java.util.List; - -/** - * List of S3 Objects - */ -@ApiStatus.Experimental -public interface S3ObjectList extends S3ObjectMetaList { - - List objects(); - - List metas(); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectMeta.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectMeta.java deleted file mode 100644 index f4e86c231..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectMeta.java +++ /dev/null @@ -1,18 +0,0 @@ -package ru.tinkoff.kora.s3.client.model; - -import org.jetbrains.annotations.ApiStatus; - -import java.time.Instant; - -/** - * S3 Object metadata representation - */ -@ApiStatus.Experimental -public interface S3ObjectMeta { - - String key(); - - Instant modified(); - - long size(); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectMetaList.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectMetaList.java deleted file mode 100644 index 35e044c33..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectMetaList.java +++ /dev/null @@ -1,16 +0,0 @@ -package ru.tinkoff.kora.s3.client.model; - -import org.jetbrains.annotations.ApiStatus; - -import java.util.List; - -/** - * List of S3 Objects metadata - */ -@ApiStatus.Experimental -public interface S3ObjectMetaList { - - String prefix(); - - List metas(); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectUpload.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectUpload.java deleted file mode 100644 index a1cfa6c54..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectUpload.java +++ /dev/null @@ -1,12 +0,0 @@ -package ru.tinkoff.kora.s3.client.model; - -import org.jetbrains.annotations.ApiStatus; - -/** - * Uploaded S3 Object metadata - */ -@ApiStatus.Experimental -public interface S3ObjectUpload { - - String versionId(); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientLogger.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientLogger.java deleted file mode 100644 index 9120719b6..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientLogger.java +++ /dev/null @@ -1,85 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import ru.tinkoff.kora.logging.common.arg.StructuredArgument; -import ru.tinkoff.kora.s3.client.S3Exception; - -import java.time.Duration; -import java.time.temporal.ChronoUnit; - -public class DefaultS3ClientLogger implements S3ClientLogger { - - private final Logger requestLogger; - private final Logger responseLogger; - - public DefaultS3ClientLogger(Class client) { - this.requestLogger = LoggerFactory.getLogger(client.getCanonicalName() + ".request"); - this.responseLogger = LoggerFactory.getLogger(client.getCanonicalName() + ".response"); - } - - @Override - public void logRequest(String method, - String bucket, - @Nullable String key, - @Nullable Long contentLength) { - if (requestLogger.isInfoEnabled()) { - var marker = StructuredArgument.marker("s3Request", gen -> { - gen.writeStartObject(); - gen.writeStringField("method", method); - gen.writeStringField("bucket", bucket); - if(key != null) { - gen.writeStringField("key", key); - } - if (contentLength != null) { - gen.writeNumberField("contentLength", contentLength); - } - gen.writeEndObject(); - }); - - if(key == null) { - this.requestLogger.info(marker, "S3 Client starting operation for {} {}", method, bucket); - } else { - this.requestLogger.info(marker, "S3 Client starting operation for {} {}/{}", method, bucket, key); - } - } - } - - @Override - public void logResponse(String method, - String bucket, - @Nullable String key, - int statusCode, - long processingTimeNanos, - @Nullable S3Exception exception) { - if (responseLogger.isInfoEnabled()) { - var marker = StructuredArgument.marker("s3Response", gen -> { - gen.writeStartObject(); - gen.writeStringField("method", method); - gen.writeStringField("bucket", bucket); - if(key != null) { - gen.writeStringField("key", key); - } - gen.writeNumberField("statusCode", statusCode); - gen.writeNumberField("processingTime", processingTimeNanos / 1_000_000); - if (exception != null) { - gen.writeStringField("errorCode", exception.getErrorCode()); - final String exType = (exception.getCause() == null) - ? exception.getClass().getCanonicalName() - : exception.getCause().getClass().getCanonicalName(); - gen.writeStringField("exceptionType", exType); - } - gen.writeEndObject(); - }); - - if(key == null) { - this.responseLogger.info(marker, "S3 Client finished operation with statusCode {} for {} {} in {}", - statusCode, method, bucket, Duration.ofNanos(processingTimeNanos).truncatedTo(ChronoUnit.MILLIS)); - } else { - this.responseLogger.info(marker, "S3 Client finished operation with statusCode {} for {} {}/{} in {}", - statusCode, method, bucket, key, Duration.ofNanos(processingTimeNanos).truncatedTo(ChronoUnit.MILLIS)); - } - } - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientTelemetry.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientTelemetry.java deleted file mode 100644 index 16b080e84..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientTelemetry.java +++ /dev/null @@ -1,58 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.s3.client.S3Exception; - -public final class DefaultS3ClientTelemetry implements S3ClientTelemetry { - - private final S3ClientLogger logger; - private final S3ClientMetrics metrics; - private final S3ClientTracer tracer; - - public DefaultS3ClientTelemetry(@Nullable S3ClientTracer tracer, - @Nullable S3ClientMetrics metrics, - @Nullable S3ClientLogger logger) { - this.logger = logger; - this.tracer = tracer; - this.metrics = metrics; - } - - @Override - public S3ClientTelemetryContext get() { - var start = System.nanoTime(); - final S3ClientTracer.S3ClientSpan span; - if (tracer != null) { - span = tracer.createSpan(); - } else { - span = null; - } - - return new S3ClientTelemetryContext() { - - @Override - public void prepared(String method, String bucket, @Nullable String key, @Nullable Long contentLength) { - if (logger != null) { - logger.logRequest(method, bucket, key, contentLength); - } - if (span != null) { - span.prepared(method, bucket, key, contentLength); - } - } - - @Override - public void close(String method, String bucket, @Nullable String key, int statusCode, @Nullable S3Exception exception) { - var end = System.nanoTime(); - var processingTime = end - start; - if (metrics != null) { - metrics.record(method, bucket, key, statusCode, processingTime, exception); - } - if (logger != null) { - logger.logResponse(method, bucket, key, statusCode, processingTime, exception); - } - if (span != null) { - span.close(statusCode, exception); - } - } - }; - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientTelemetryFactory.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientTelemetryFactory.java deleted file mode 100644 index 7c939b89f..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientTelemetryFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.s3.client.S3Exception; -import ru.tinkoff.kora.telemetry.common.TelemetryConfig; - -public final class DefaultS3ClientTelemetryFactory implements S3ClientTelemetryFactory { - - private static final S3ClientTelemetry.S3ClientTelemetryContext EMPTY_CTX = new S3ClientTelemetry.S3ClientTelemetryContext() { - @Override - public void prepared(String method, String bucket, String key, Long contentLength) {} - - @Override - public void close(String method, String bucket, String key, int statusCode, @Nullable S3Exception exception) {} - }; - private static final S3ClientTelemetry EMPTY_TELEMETRY = () -> EMPTY_CTX; - - private final S3ClientLoggerFactory loggerFactory; - private final S3ClientTracerFactory tracingFactory; - private final S3ClientMetricsFactory metricsFactory; - - public DefaultS3ClientTelemetryFactory(@Nullable S3ClientLoggerFactory loggerFactory, - @Nullable S3ClientTracerFactory tracingFactory, - @Nullable S3ClientMetricsFactory metricsFactory) { - this.loggerFactory = loggerFactory; - this.tracingFactory = tracingFactory; - this.metricsFactory = metricsFactory; - } - - @Override - public S3ClientTelemetry get(TelemetryConfig config, Class client) { - var logger = this.loggerFactory == null ? null : this.loggerFactory.get(config.logging(), client); - var metrics = this.metricsFactory == null ? null : this.metricsFactory.get(config.metrics(), client); - var tracer = this.tracingFactory == null ? null : this.tracingFactory.get(config.tracing(), client); - if (metrics == null && tracer == null && logger == null) { - return EMPTY_TELEMETRY; - } - - return new DefaultS3ClientTelemetry(tracer, metrics, logger); - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientLogger.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientLogger.java deleted file mode 100644 index f2a7b7a1a..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientLogger.java +++ /dev/null @@ -1,75 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import ru.tinkoff.kora.logging.common.arg.StructuredArgument; -import ru.tinkoff.kora.s3.client.S3Exception; - -import java.time.Duration; -import java.time.temporal.ChronoUnit; - -public class DefaultS3KoraClientLogger implements S3KoraClientLogger { - - private final Logger requestLogger; - private final Logger responseLogger; - - public DefaultS3KoraClientLogger(Class clientImpl) { - this.requestLogger = LoggerFactory.getLogger(clientImpl.getCanonicalName() + ".request"); - this.responseLogger = LoggerFactory.getLogger(clientImpl.getCanonicalName() + ".response"); - } - - @Override - public void logRequest(String operation, - String bucket, - @Nullable String key, - @Nullable Long contentLength) { - if (requestLogger.isInfoEnabled()) { - var marker = StructuredArgument.marker("s3Operation", gen -> { - gen.writeStartObject(); - gen.writeStringField("operation", operation); - gen.writeStringField("bucket", bucket); - if (key != null) { - gen.writeStringField("key", key); - } - if (contentLength != null) { - gen.writeNumberField("contentLength", contentLength); - } - gen.writeEndObject(); - }); - - this.requestLogger.info(marker, "S3 Kora Client starting operation {} for bucket {}", operation, bucket); - } - } - - @Override - public void logResponse(String operation, - String bucket, - @Nullable String key, - long processingTimeNanos, - @Nullable S3Exception exception) { - if (responseLogger.isInfoEnabled()) { - var marker = StructuredArgument.marker("s3Operation", gen -> { - gen.writeStartObject(); - gen.writeStringField("operation", operation); - gen.writeStringField("bucket", bucket); - if (key != null) { - gen.writeStringField("key", key); - } - gen.writeStringField("status", (exception == null) ? "success" : "failure"); - gen.writeNumberField("processingTime", processingTimeNanos / 1_000_000); - if (exception != null) { - gen.writeStringField("errorCode", exception.getErrorCode()); - final String exType = (exception.getCause() == null) - ? exception.getClass().getCanonicalName() - : exception.getCause().getClass().getCanonicalName(); - gen.writeStringField("exceptionType", exType); - } - gen.writeEndObject(); - }); - - responseLogger.info(marker, "S3 Kora Client finished operation {} for bucket {} in {}", - operation, bucket, Duration.ofNanos(processingTimeNanos).truncatedTo(ChronoUnit.MILLIS)); - } - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientLoggerFactory.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientLoggerFactory.java deleted file mode 100644 index e72cd8710..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientLoggerFactory.java +++ /dev/null @@ -1,17 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import ru.tinkoff.kora.telemetry.common.TelemetryConfig; - -import java.util.Objects; - -public class DefaultS3KoraClientLoggerFactory implements S3KoraClientLoggerFactory { - - @Override - public S3KoraClientLogger get(TelemetryConfig.LogConfig logging, Class clientImpl) { - if (Objects.requireNonNullElse(logging.enabled(), false)) { - return new DefaultS3KoraClientLogger(clientImpl); - } else { - return null; - } - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientTelemetry.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientTelemetry.java deleted file mode 100644 index 71cce37ca..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientTelemetry.java +++ /dev/null @@ -1,46 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; - -public final class DefaultS3KoraClientTelemetry implements S3KoraClientTelemetry { - - private final S3KoraClientLogger logger; - private final S3KoraClientMetrics metrics; - private final S3KoraClientTracer tracer; - - public DefaultS3KoraClientTelemetry(@Nullable S3KoraClientLogger logger, - @Nullable S3KoraClientMetrics metrics, - @Nullable S3KoraClientTracer tracer) { - this.logger = logger; - this.metrics = metrics; - this.tracer = tracer; - } - - @Override - public S3KoraClientTelemetryContext get(String operation, - String bucket, - @Nullable String key, - @Nullable Long contentLength) { - var start = System.nanoTime(); - final S3KoraClientTracer.S3KoraClientSpan span; - if (tracer != null) { - span = tracer.createSpan(operation, bucket, key, contentLength); - } else { - span = null; - } - - return exception -> { - var end = System.nanoTime(); - var processingTime = end - start; - if (metrics != null) { - metrics.record(operation, bucket, key, processingTime, exception); - } - if (logger != null) { - logger.logResponse(operation, bucket, key, processingTime, exception); - } - if (span != null) { - span.close(exception); - } - }; - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientTelemetryFactory.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientTelemetryFactory.java deleted file mode 100644 index 06eb844a0..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3KoraClientTelemetryFactory.java +++ /dev/null @@ -1,34 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.telemetry.common.TelemetryConfig; - -public final class DefaultS3KoraClientTelemetryFactory implements S3KoraClientTelemetryFactory { - - private static final S3KoraClientTelemetry.S3KoraClientTelemetryContext EMPTY_CTX = exception -> {}; - private static final S3KoraClientTelemetry EMPTY_TELEMETRY = (operation, bucket, key, contentLength) -> EMPTY_CTX; - - private final S3KoraClientLoggerFactory loggerFactory; - private final S3KoraClientTracerFactory tracingFactory; - private final S3KoraClientMetricsFactory metricsFactory; - - public DefaultS3KoraClientTelemetryFactory(@Nullable S3KoraClientLoggerFactory loggerFactory, - @Nullable S3KoraClientTracerFactory tracingFactory, - @Nullable S3KoraClientMetricsFactory metricsFactory) { - this.loggerFactory = loggerFactory; - this.tracingFactory = tracingFactory; - this.metricsFactory = metricsFactory; - } - - @Override - public S3KoraClientTelemetry get(TelemetryConfig config, Class clientImpl) { - var logger = this.loggerFactory == null ? null : this.loggerFactory.get(config.logging(), clientImpl); - var metrics = this.metricsFactory == null ? null : this.metricsFactory.get(config.metrics(), clientImpl); - var tracer = this.tracingFactory == null ? null : this.tracingFactory.get(config.tracing(), clientImpl); - if (metrics == null && tracer == null && logger == null) { - return EMPTY_TELEMETRY; - } - - return new DefaultS3KoraClientTelemetry(logger, metrics, tracer); - } -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientLogger.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientLogger.java deleted file mode 100644 index 8e72a5b68..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientLogger.java +++ /dev/null @@ -1,19 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.s3.client.S3Exception; - -public interface S3ClientLogger { - - void logRequest(String method, - String bucket, - @Nullable String key, - @Nullable Long contentLength); - - void logResponse(String method, - String bucket, - @Nullable String key, - int statusCode, - long processingTimeNanos, - @Nullable S3Exception exception); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientMetrics.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientMetrics.java deleted file mode 100644 index 0d6a36ff6..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientMetrics.java +++ /dev/null @@ -1,14 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.s3.client.S3Exception; - -public interface S3ClientMetrics { - - void record(String method, - String bucket, - @Nullable String key, - int statusCode, - long processingTimeNanos, - @Nullable S3Exception exception); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTelemetry.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTelemetry.java deleted file mode 100644 index 4b67a3080..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTelemetry.java +++ /dev/null @@ -1,30 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.s3.client.S3Exception; - -public interface S3ClientTelemetry { - - interface S3ClientTelemetryContext { - - void prepared(String method, - String bucket, - @Nullable String key, - @Nullable Long contentLength); - - default void close(String method, - String bucket, - @Nullable String key, - int statusCode) { - close(method, bucket, key, statusCode, null); - } - - void close(String method, - String bucket, - @Nullable String key, - int statusCode, - @Nullable S3Exception exception); - } - - S3ClientTelemetryContext get(); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTelemetryFactory.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTelemetryFactory.java deleted file mode 100644 index 20a6eefb0..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTelemetryFactory.java +++ /dev/null @@ -1,8 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import ru.tinkoff.kora.telemetry.common.TelemetryConfig; - -public interface S3ClientTelemetryFactory { - - S3ClientTelemetry get(TelemetryConfig config, Class client); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTracer.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTracer.java deleted file mode 100644 index 1823d1a10..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTracer.java +++ /dev/null @@ -1,19 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.s3.client.S3Exception; - -public interface S3ClientTracer { - - interface S3ClientSpan { - - void prepared(String method, - String bucket, - @Nullable String key, - @Nullable Long contentLength); - - void close(int statusCode, @Nullable S3Exception exception); - } - - S3ClientSpan createSpan(); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientLogger.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientLogger.java deleted file mode 100644 index 6be652d4c..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientLogger.java +++ /dev/null @@ -1,18 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.s3.client.S3Exception; - -public interface S3KoraClientLogger { - - void logRequest(String operation, - String bucket, - @Nullable String key, - @Nullable Long contentLength); - - void logResponse(String operation, - String bucket, - @Nullable String key, - long processingTimeNanos, - @Nullable S3Exception exception); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientLoggerFactory.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientLoggerFactory.java deleted file mode 100644 index c02a1a38f..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientLoggerFactory.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.telemetry.common.TelemetryConfig; - -public interface S3KoraClientLoggerFactory { - - @Nullable - S3KoraClientLogger get(TelemetryConfig.LogConfig logging, Class clientImpl); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientMetrics.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientMetrics.java deleted file mode 100644 index a00ea2424..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientMetrics.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.s3.client.S3Exception; - -public interface S3KoraClientMetrics { - - void record(String operation, - String bucket, - @Nullable String key, - long processingTimeNanos, - @Nullable S3Exception exception); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientMetricsFactory.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientMetricsFactory.java deleted file mode 100644 index 54c3edf2b..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientMetricsFactory.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.telemetry.common.TelemetryConfig; - -public interface S3KoraClientMetricsFactory { - - @Nullable - S3KoraClientMetrics get(TelemetryConfig.MetricsConfig metrics, Class clientImpl); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTelemetry.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTelemetry.java deleted file mode 100644 index 236013dac..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTelemetry.java +++ /dev/null @@ -1,21 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.s3.client.S3Exception; - -public interface S3KoraClientTelemetry { - - interface S3KoraClientTelemetryContext { - - default void close() { - close(null); - } - - void close(@Nullable S3Exception exception); - } - - S3KoraClientTelemetryContext get(String operation, - String bucket, - @Nullable String key, - @Nullable Long contentLength); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTelemetryFactory.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTelemetryFactory.java deleted file mode 100644 index ad9dfaa0b..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTelemetryFactory.java +++ /dev/null @@ -1,8 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import ru.tinkoff.kora.telemetry.common.TelemetryConfig; - -public interface S3KoraClientTelemetryFactory { - - S3KoraClientTelemetry get(TelemetryConfig config, Class clientImpl); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTracer.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTracer.java deleted file mode 100644 index 51e5d6797..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTracer.java +++ /dev/null @@ -1,17 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.s3.client.S3Exception; - -public interface S3KoraClientTracer { - - interface S3KoraClientSpan { - - void close(@Nullable S3Exception exception); - } - - S3KoraClientSpan createSpan(String operation, - String bucket, - @Nullable String key, - @Nullable Long contentLength); -} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTracerFactory.java b/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTracerFactory.java deleted file mode 100644 index f97d7c532..000000000 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3KoraClientTracerFactory.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.tinkoff.kora.s3.client.telemetry; - -import jakarta.annotation.Nullable; -import ru.tinkoff.kora.telemetry.common.TelemetryConfig; - -public interface S3KoraClientTracerFactory { - - @Nullable - S3KoraClientTracer get(TelemetryConfig.TracingConfig tracing, Class clientImpl); -} diff --git a/experimental/s3-client-minio/build.gradle b/experimental/s3-client-minio/build.gradle deleted file mode 100644 index 19bfd5f4b..000000000 --- a/experimental/s3-client-minio/build.gradle +++ /dev/null @@ -1,15 +0,0 @@ -apply from: "${project.rootDir}/gradle/in-test-generated.gradle" - -dependencies { - annotationProcessor project(":config:config-annotation-processor") - - compileOnly libs.jetbrains.annotations - - api project(":experimental:s3-client-common") - api libs.s3client.minio - - implementation project(":config:config-common") - - testImplementation project(":internal:test-logging") - testImplementation libs.testcontainers.junit.jupiter -} diff --git a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3Body.java b/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3Body.java deleted file mode 100644 index d192a9660..000000000 --- a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3Body.java +++ /dev/null @@ -1,58 +0,0 @@ -package ru.tinkoff.kora.s3.client.minio; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.s3.client.model.S3Body; - -import java.io.InputStream; -import java.net.http.HttpRequest; -import java.nio.ByteBuffer; -import java.util.concurrent.Flow; - -@ApiStatus.Experimental -final class MinioS3Body implements S3Body { - - private final String encoding; - private final String type; - private final long size; - private final InputStream inputStream; - - public MinioS3Body(InputStream inputStream, long size, String encoding, String type) { - this.encoding = encoding; - this.type = type; - this.size = size; - this.inputStream = inputStream; - } - - @Override - public InputStream asInputStream() { - return inputStream; - } - - @Override - public Flow.Publisher asPublisher() { - return HttpRequest.BodyPublishers.ofInputStream(() -> inputStream); - } - - @Override - public long size() { - return size; - } - - @Override - public String encoding() { - return encoding; - } - - @Override - public String type() { - return type; - } - - @Override - public String toString() { - return "MinioS3Body{type=" + type + - ", encoding=" + encoding + - ", size=" + size + - '}'; - } -} diff --git a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ClientConfig.java b/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ClientConfig.java deleted file mode 100644 index d3743a1fe..000000000 --- a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ClientConfig.java +++ /dev/null @@ -1,35 +0,0 @@ -package ru.tinkoff.kora.s3.client.minio; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.common.util.Size; -import ru.tinkoff.kora.config.common.annotation.ConfigValueExtractor; - -import java.time.Duration; - -@ApiStatus.Experimental -@ConfigValueExtractor -public interface MinioS3ClientConfig { - - enum AddressStyle { - PATH, - VIRTUAL_HOSTED - } - - default AddressStyle addressStyle() { - return AddressStyle.PATH; - } - - default Duration requestTimeout() { - return Duration.ofSeconds(45); - } - - UploadConfig upload(); - - @ConfigValueExtractor - interface UploadConfig { - - default Size partSize() { - return Size.of(8, Size.Type.MiB); - } - } -} diff --git a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ClientModule.java b/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ClientModule.java deleted file mode 100644 index 9214f2dac..000000000 --- a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ClientModule.java +++ /dev/null @@ -1,108 +0,0 @@ -package ru.tinkoff.kora.s3.client.minio; - -import io.minio.MinioAsyncClient; -import io.minio.MinioClient; -import io.minio.credentials.Provider; -import io.minio.credentials.StaticProvider; -import io.minio.http.HttpUtils; -import jakarta.annotation.Nullable; -import okhttp3.OkHttpClient; -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.common.DefaultComponent; -import ru.tinkoff.kora.common.Tag; -import ru.tinkoff.kora.config.common.Config; -import ru.tinkoff.kora.config.common.extractor.ConfigValueExtractor; -import ru.tinkoff.kora.s3.client.S3ClientModule; -import ru.tinkoff.kora.s3.client.S3Config; -import ru.tinkoff.kora.s3.client.S3KoraAsyncClient; -import ru.tinkoff.kora.s3.client.S3KoraClient; -import ru.tinkoff.kora.s3.client.telemetry.S3ClientTelemetryFactory; -import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientTelemetryFactory; - -import java.util.concurrent.TimeUnit; - -@ApiStatus.Experimental -public interface MinioS3ClientModule extends S3ClientModule { - - @DefaultComponent - default Provider minioCredentialProvider(S3Config s3Config) { - return new StaticProvider(s3Config.accessKey(), s3Config.secretKey(), null); - } - - default MinioS3ClientConfig minioS3ClientConfig(Config config, ConfigValueExtractor extractor) { - var value = config.get("s3client.minio"); - return extractor.extract(value); - } - - @Tag(MinioClient.class) - @DefaultComponent - default OkHttpClient minioOkHttpClient(@Nullable OkHttpClient okHttpClient, - MinioS3ClientConfig minioS3ClientConfig) { - long timeout = minioS3ClientConfig.requestTimeout().toMillis(); - return (okHttpClient == null) - ? HttpUtils.newDefaultHttpClient(timeout, timeout, timeout) - : okHttpClient.newBuilder().callTimeout(timeout, TimeUnit.MILLISECONDS).build(); - } - - default MinioClient minioClient(S3Config s3Config, - MinioS3ClientConfig minioS3ClientConfig, - Provider provider, - S3ClientTelemetryFactory telemetryFactory, - @Tag(MinioClient.class) OkHttpClient okHttpClient) { - MinioClient builded = MinioClient.builder() - .endpoint(s3Config.url()) - .region(s3Config.region()) - .credentialsProvider(provider) - .httpClient(okHttpClient.newBuilder() - .addInterceptor(new MinioS3ClientTelemetryInterceptor(telemetryFactory.get(s3Config.telemetry(), MinioClient.class), minioS3ClientConfig.addressStyle())) - .build()) - .build(); - - if (minioS3ClientConfig.addressStyle() == MinioS3ClientConfig.AddressStyle.PATH) { - builded.disableVirtualStyleEndpoint(); - } else { - builded.enableVirtualStyleEndpoint(); - } - - return builded; - } - - default MinioAsyncClient minioAsyncClient(S3Config s3Config, - MinioS3ClientConfig minioS3ClientConfig, - Provider provider, - S3ClientTelemetryFactory telemetryFactory, - @Tag(MinioClient.class) OkHttpClient okHttpClient) { - MinioAsyncClient builded = MinioAsyncClient.builder() - .endpoint(s3Config.url()) - .region(s3Config.region()) - .credentialsProvider(provider) - .httpClient(okHttpClient.newBuilder() - .addInterceptor(new MinioS3ClientTelemetryInterceptor(telemetryFactory.get(s3Config.telemetry(), MinioAsyncClient.class), minioS3ClientConfig.addressStyle())) - .build()) - .build(); - - if (minioS3ClientConfig.addressStyle() == MinioS3ClientConfig.AddressStyle.PATH) { - builded.disableVirtualStyleEndpoint(); - } else { - builded.enableVirtualStyleEndpoint(); - } - - return builded; - } - - default S3KoraClient MinioS3KoraClient(MinioClient minioClient, - MinioS3ClientConfig minioS3ClientConfig, - S3Config s3Config, - S3KoraClientTelemetryFactory telemetryFactory) { - var telemetry = telemetryFactory.get(s3Config.telemetry(), MinioClient.class); - return new MinioS3KoraClient(minioClient, minioS3ClientConfig, telemetry); - } - - default S3KoraAsyncClient MinioS3KoraAsyncClient(MinioAsyncClient minioAsyncClient, - MinioS3ClientConfig minioS3ClientConfig, - S3Config s3Config, - S3KoraClientTelemetryFactory telemetryFactory) { - var telemetry = telemetryFactory.get(s3Config.telemetry(), MinioAsyncClient.class); - return new MinioS3KoraAsyncClient(minioAsyncClient, minioS3ClientConfig, telemetry); - } -} diff --git a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ClientTelemetryInterceptor.java b/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ClientTelemetryInterceptor.java deleted file mode 100644 index 8fe66d729..000000000 --- a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ClientTelemetryInterceptor.java +++ /dev/null @@ -1,120 +0,0 @@ -package ru.tinkoff.kora.s3.client.minio; - -import io.minio.errors.ErrorResponseException; -import jakarta.annotation.Nonnull; -import okhttp3.HttpUrl; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; -import ru.tinkoff.kora.s3.client.S3Exception; -import ru.tinkoff.kora.s3.client.telemetry.S3ClientTelemetry; -import ru.tinkoff.kora.s3.client.telemetry.S3ClientTelemetry.S3ClientTelemetryContext; - -import java.io.IOException; -import java.util.concurrent.CompletionException; - -@ApiStatus.Experimental -public final class MinioS3ClientTelemetryInterceptor implements Interceptor { - - private final S3ClientTelemetry telemetry; - private final MinioS3ClientConfig.AddressStyle addressStyle; - - public MinioS3ClientTelemetryInterceptor(S3ClientTelemetry telemetry, MinioS3ClientConfig.AddressStyle addressStyle) { - this.telemetry = telemetry; - this.addressStyle = addressStyle; - } - - @Nonnull - @Override - public Response intercept(@Nonnull Chain chain) throws IOException { - final S3ClientTelemetryContext telemetryContext = telemetry.get(); - - final Request request = chain.request(); - Long contentLength = (request.body() == null) - ? null - : request.body().contentLength(); - - final HttpUrl url = request.url(); - final BucketAndKey bk; - if (addressStyle == MinioS3ClientConfig.AddressStyle.PATH) { - bk = getPathAddress(url.encodedPath()); - } else { - bk = getVirtualHost(url.host(), url.encodedPath()); - } - - telemetryContext.prepared(request.method(), bk.bucket(), bk.key(), contentLength); - - try { - final Response response = chain.proceed(request); - telemetryContext.close(request.method(), bk.bucket(), bk.key(), response.code()); - return response; - } catch (Exception e) { - Throwable cause = e; - if (cause instanceof CompletionException ce) { - cause = ce.getCause(); - } - - final S3Exception ex; - final int code; - if (cause instanceof ErrorResponseException re) { - code = re.response().code(); - ex = new S3Exception(re, re.errorResponse().code(), re.errorResponse().message()); - } else { - code = -1; - ex = new S3Exception(cause, cause.getClass().getSimpleName(), cause.getMessage()); - } - - telemetryContext.close(request.method(), bk.bucket(), bk.key(), code, ex); - throw e; - } - } - - private record BucketAndKey(String bucket, @Nullable String key) {} - - private static BucketAndKey getPathAddress(String path) { - final String bucket; - final String key; - final int startFrom = (path.charAt(0) == '/') - ? 1 - : 0; - - if (path.equals("/")) { - bucket = "/"; - key = null; - } else { - int bucketSeparator = path.indexOf('/', startFrom); - if (bucketSeparator == -1) { - bucket = path.substring(startFrom); - key = null; - } else { - bucket = path.substring(startFrom, bucketSeparator); - key = path.substring(bucketSeparator + 1); - } - } - - return new BucketAndKey(bucket, key); - } - - private static BucketAndKey getVirtualHost(String host, String path) { - int bucketEnd = host.indexOf('.'); - if (bucketEnd == -1) { - return getPathAddress(path); - } - - final String bucket; - final String key; - final int startFrom = (path.charAt(0) == '/') - ? 1 - : 0; - bucket = host.substring(0, bucketEnd); - if (path.length() == 1) { - key = null; - } else { - key = path.substring(startFrom); - } - - return new BucketAndKey(bucket, key); - } -} diff --git a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3KoraAsyncClient.java b/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3KoraAsyncClient.java deleted file mode 100644 index bfe1df8c5..000000000 --- a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3KoraAsyncClient.java +++ /dev/null @@ -1,368 +0,0 @@ -package ru.tinkoff.kora.s3.client.minio; - -import io.minio.*; -import io.minio.errors.ErrorResponseException; -import io.minio.messages.DeleteError; -import io.minio.messages.DeleteObject; -import io.minio.messages.Item; -import jakarta.annotation.Nullable; -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.common.Context; -import ru.tinkoff.kora.s3.client.S3DeleteException; -import ru.tinkoff.kora.s3.client.S3Exception; -import ru.tinkoff.kora.s3.client.S3KoraAsyncClient; -import ru.tinkoff.kora.s3.client.S3NotFoundException; -import ru.tinkoff.kora.s3.client.model.*; -import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientTelemetry; - -import java.io.ByteArrayInputStream; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; -import java.util.function.Supplier; - -@ApiStatus.Experimental -public class MinioS3KoraAsyncClient implements S3KoraAsyncClient { - - private final MinioAsyncClient minioClient; - private final MinioS3ClientConfig minioS3ClientConfig; - private final S3KoraClientTelemetry telemetry; - - public MinioS3KoraAsyncClient(MinioAsyncClient minioClient, - MinioS3ClientConfig minioS3ClientConfig, - S3KoraClientTelemetry telemetry) { - this.minioClient = minioClient; - this.minioS3ClientConfig = minioS3ClientConfig; - this.telemetry = telemetry; - } - - private CompletionStage getInternal(String bucket, String key) { - try { - return minioClient.getObject(GetObjectArgs.builder() - .bucket(bucket) - .object(key) - .build()) - .thenApply(MinioS3Object::new); - } catch (Exception e) { - return handleExceptionStage(e); - } - } - - @Override - public CompletionStage get(String bucket, String key) { - return wrapWithTelemetry(getInternal(bucket, key), - () -> telemetry.get("GetObject", bucket, key, null)); - } - - private CompletionStage getMetaInternal(String bucket, String key) { - try { - return minioClient.statObject(StatObjectArgs.builder() - .bucket(bucket) - .object(key) - .build()) - .thenApply(MinioS3ObjectMeta::new); - } catch (Exception e) { - return handleExceptionStage(e); - } - } - - @Override - public CompletionStage getMeta(String bucket, String key) { - return wrapWithTelemetry(getMetaInternal(bucket, key), - () -> telemetry.get("GetObjectMeta", bucket, key, null)); - } - - @Override - public CompletionStage> get(String bucket, Collection keys) { - var futures = keys.stream() - .map(k -> getInternal(bucket, k).toCompletableFuture()) - .toArray(CompletableFuture[]::new); - - var operation = CompletableFuture.allOf(futures) - .thenApply(_v -> Arrays.stream(futures) - .map(f -> ((S3Object) f.join())) - .toList()) - .exceptionallyCompose(MinioS3KoraAsyncClient::handleExceptionStage); - - return wrapWithTelemetry(operation, - () -> telemetry.get("GetObjects", bucket, null, null)); - } - - @Override - public CompletionStage> getMeta(String bucket, Collection keys) { - var futures = keys.stream() - .map(k -> getMetaInternal(bucket, k).toCompletableFuture()) - .toArray(CompletableFuture[]::new); - - var operation = CompletableFuture.allOf(futures) - .thenApply(_v -> Arrays.stream(futures) - .map(f -> ((S3ObjectMeta) f.join())) - .toList()) - .exceptionallyCompose(MinioS3KoraAsyncClient::handleExceptionStage); - - return wrapWithTelemetry(operation, - () -> telemetry.get("GetObjectMetas", bucket, null, null)); - } - - @Override - public CompletionStage list(String bucket, String prefix, @Nullable String delimiter, int limit) { - return wrapWithTelemetry(fork -> listInternal(bucket, prefix, delimiter, limit, fork), - () -> telemetry.get("ListObjects", bucket, prefix, null)); - } - - private CompletionStage listInternal(String bucket, String prefix, @Nullable String delimiter, int limit, Context context) { - return listMetaInternal(bucket, prefix, delimiter, limit, context) - .thenCompose(metaList -> { - try { - context.inject(); - - var futures = metaList.metas().stream() - .map(meta -> getInternal(bucket, meta.key()).toCompletableFuture()) - .toArray(CompletableFuture[]::new); - - return CompletableFuture.allOf(futures) - .thenApply(_v -> { - final List objects = new ArrayList<>(futures.length); - for (var future : futures) { - objects.add(((S3Object) future.join())); - } - - return new MinioS3ObjectList(metaList, objects); - }); - } finally { - Context.clear(); - } - }); - } - - private CompletionStage listMetaInternal(String bucket, String prefix, @Nullable String delimiter, int limit, Context context) { - return CompletableFuture.supplyAsync(() -> { - try { - context.inject(); - - var response = minioClient.listObjects(ListObjectsArgs.builder() - .bucket(bucket) - .prefix(prefix) - .maxKeys(limit) - .delimiter(delimiter) - .build()); - - final List metas = new ArrayList<>(); - for (Result result : response) { - Item item = result.get(); - metas.add(new MinioS3ObjectMeta(item)); - } - - return new MinioS3ObjectMetaList(prefix, metas); - } catch (Exception e) { - throw handleException(e); - } finally { - Context.clear(); - } - }) - .exceptionallyCompose(MinioS3KoraAsyncClient::handleExceptionStage); - } - - @Override - public CompletionStage listMeta(String bucket, String prefix, @Nullable String delimiter, int limit) { - return wrapWithTelemetry(fork -> listMetaInternal(bucket, prefix, delimiter, limit, fork), - () -> telemetry.get("ListObjectMetas", bucket, prefix, null)); - } - - @Override - public CompletionStage> list(String bucket, Collection prefixes, @Nullable String delimiter, int limitPerPrefix) { - return wrapWithTelemetry(fork -> { - var futures = prefixes.stream() - .map(p -> listInternal(bucket, p, delimiter, limitPerPrefix, fork).toCompletableFuture()) - .toArray(CompletableFuture[]::new); - - return CompletableFuture.allOf(futures) - .thenApply(_v -> Arrays.stream(futures) - .map(f -> ((S3ObjectList) f.join())) - .toList()); - }, () -> telemetry.get("ListMultiObjects", bucket, null, null)); - } - - @Override - public CompletionStage> listMeta(String bucket, Collection prefixes, @Nullable String delimiter, int limitPerPrefix) { - return wrapWithTelemetry(fork -> { - var futures = prefixes.stream() - .map(p -> listMetaInternal(bucket, p, delimiter, limitPerPrefix, fork).toCompletableFuture()) - .toArray(CompletableFuture[]::new); - - return CompletableFuture.allOf(futures) - .thenApply(_v -> Arrays.stream(futures) - .map(f -> ((S3ObjectMetaList) f.join())) - .toList()); - }, () -> telemetry.get("ListMultiObjectMetas", bucket, null, null)); - } - - @Override - public CompletionStage put(String bucket, String key, S3Body body) { - var requestBuilder = PutObjectArgs.builder() - .bucket(bucket) - .object(key) - .contentType(body.type() == null ? "application/octet-stream" : body.type()); - - if (body.size() > 0 && body.encoding() != null) { - requestBuilder.headers(Map.of( - "content-encoding", String.valueOf(body.encoding()), - "content-length", String.valueOf(body.size()) - )); - } else if (body.size() > 0) { - requestBuilder.headers(Map.of( - "content-length", String.valueOf(body.size()) - )); - } else if (body.encoding() != null) { - requestBuilder.headers(Map.of( - "content-encoding", String.valueOf(body.encoding()) - )); - } - - var ctx = Context.current(); - try { - var fork = ctx.fork(); - fork.inject(); - - var size = body.size() > 0 ? body.size() : null; - var context = telemetry.get("PutObject", bucket, key, size); - - final CompletionStage operation; - try { - if (body instanceof ByteS3Body bb) { - operation = minioClient.putObject(requestBuilder.stream(new ByteArrayInputStream(bb.bytes()), bb.size(), -1).build()) - .thenApply(r -> new MinioS3ObjectUpload(r.versionId())); - } else if (body.size() > 0) { - operation = minioClient.putObject(requestBuilder.stream(body.asInputStream(), body.size(), minioS3ClientConfig.upload().partSize().toBytes()).build()) - .thenApply(r -> new MinioS3ObjectUpload(r.versionId())); - } else { - operation = minioClient.putObject(requestBuilder.stream(body.asInputStream(), -1, minioS3ClientConfig.upload().partSize().toBytes()).build()) - .thenApply(r -> new MinioS3ObjectUpload(r.versionId())); - } - } catch (Exception e) { - S3Exception ex = handleException(e); - context.close(ex); - return CompletableFuture.failedFuture(ex); - } - - return operation - .exceptionallyCompose(MinioS3KoraAsyncClient::handleExceptionStage) - .whenComplete((r, e) -> { - if (e != null) { - context.close(handleException(e)); - } else { - context.close(); - } - }); - } finally { - ctx.inject(); - } - } - - @Override - public CompletionStage delete(String bucket, String key) { - return wrapWithTelemetry(fork -> minioClient.removeObject(RemoveObjectArgs.builder() - .bucket(bucket) - .object(key) - .build()), - () -> telemetry.get("DeleteObject", bucket, key, null)); - } - - @Override - public CompletionStage delete(String bucket, Collection keys) { - return wrapWithTelemetry(fork -> CompletableFuture.supplyAsync(() -> { - try { - fork.inject(); - var response = minioClient.removeObjects(RemoveObjectsArgs.builder() - .bucket(bucket) - .objects(keys.stream() - .map(DeleteObject::new) - .toList()) - .build()); - - final List errors = new ArrayList<>(keys.size()); - for (Result result : response) { - DeleteError er = result.get(); - errors.add(new S3DeleteException.Error(er.objectName(), er.bucketName(), er.code(), er.message())); - } - if (!errors.isEmpty()) { - throw new S3DeleteException(errors); - } - return null; - } catch (Exception e) { - throw handleException(e); - } finally { - Context.clear(); - } - }), () -> telemetry.get("DeleteObjects", bucket, null, null)); - } - - @FunctionalInterface - private interface FunctionThrowable { - - R apply(T t) throws Throwable; - } - - private static CompletionStage wrapWithTelemetry(CompletionStage operationSupplier, - Supplier contextSupplier) { - return wrapWithTelemetry(context -> operationSupplier, contextSupplier); - } - - private static CompletionStage wrapWithTelemetry(FunctionThrowable> operationSupplier, - Supplier contextSupplier) { - var ctx = Context.current(); - try { - var fork = ctx.fork(); - fork.inject(); - - var context = contextSupplier.get(); - - final CompletionStage operation; - try { - operation = operationSupplier.apply(fork); - } catch (Throwable e) { - S3Exception ex = handleException(e); - context.close(ex); - return CompletableFuture.failedFuture(ex); - } - - return operation - .exceptionallyCompose(MinioS3KoraAsyncClient::handleExceptionStage) - .whenComplete((r, e) -> { - if (e != null) { - context.close(handleException(e)); - } else { - context.close(); - } - }); - } finally { - ctx.inject(); - } - } - - private static CompletionStage handleExceptionStage(Throwable e) { - return CompletableFuture.failedFuture(handleException(e)); - } - - private static S3Exception handleException(Throwable e) { - Throwable cause = e; - if (e instanceof CompletionException ce) { - cause = ce.getCause(); - } - - if (cause instanceof S3Exception se) { - return se; - } else if (cause instanceof ErrorResponseException re) { - if ("NoSuchKey".equals(re.errorResponse().code())) { - return S3NotFoundException.ofNoSuchKey(cause, re.errorResponse().message()); - } else if ("NoSuchBucket".equals(re.errorResponse().code())) { - return S3NotFoundException.ofNoSuchBucket(cause, re.errorResponse().message()); - } else { - return new S3Exception(cause, re.errorResponse().code(), re.errorResponse().message()); - } - } else { - return new S3Exception(cause, "unknown", "unknown"); - } - } -} diff --git a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3KoraClient.java b/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3KoraClient.java deleted file mode 100644 index f20b57d55..000000000 --- a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3KoraClient.java +++ /dev/null @@ -1,332 +0,0 @@ -package ru.tinkoff.kora.s3.client.minio; - -import io.minio.*; -import io.minio.errors.ErrorResponseException; -import io.minio.messages.DeleteError; -import io.minio.messages.DeleteObject; -import io.minio.messages.Item; -import jakarta.annotation.Nullable; -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.common.Context; -import ru.tinkoff.kora.s3.client.S3DeleteException; -import ru.tinkoff.kora.s3.client.S3Exception; -import ru.tinkoff.kora.s3.client.S3KoraClient; -import ru.tinkoff.kora.s3.client.S3NotFoundException; -import ru.tinkoff.kora.s3.client.model.*; -import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientTelemetry; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletionException; -import java.util.function.Supplier; - -@ApiStatus.Experimental -public class MinioS3KoraClient implements S3KoraClient { - - private final MinioClient minioClient; - private final MinioS3ClientConfig minioS3ClientConfig; - private final S3KoraClientTelemetry telemetry; - - public MinioS3KoraClient(MinioClient minioClient, - MinioS3ClientConfig minioS3ClientConfig, - S3KoraClientTelemetry telemetry) { - this.minioClient = minioClient; - this.minioS3ClientConfig = minioS3ClientConfig; - this.telemetry = telemetry; - } - - @Override - public S3Object get(String bucket, String key) throws S3NotFoundException { - return wrapWithTelemetry(() -> getInternal(bucket, key), - () -> telemetry.get("GetObject", bucket, key, null)); - } - - private S3Object getInternal(String bucket, String key) throws S3NotFoundException { - try { - var response = minioClient.getObject(GetObjectArgs.builder() - .bucket(bucket) - .object(key) - .build()); - - return new MinioS3Object(response); - } catch (Exception e) { - throw handleException(e); - } - } - - @Override - public S3ObjectMeta getMeta(String bucket, String key) throws S3NotFoundException { - return wrapWithTelemetry(() -> getMetaInternal(bucket, key), - () -> telemetry.get("GetObjectMeta", bucket, key, null)); - } - - private S3ObjectMeta getMetaInternal(String bucket, String key) throws S3NotFoundException { - try { - var response = minioClient.statObject(StatObjectArgs.builder() - .bucket(bucket) - .object(key) - .build()); - - return new MinioS3ObjectMeta(response); - } catch (Exception e) { - throw handleException(e); - } - } - - @Override - public List get(String bucket, Collection keys) { - return wrapWithTelemetry(() -> { - final List objects = new ArrayList<>(keys.size()); - for (String key : keys) { - try { - S3Object object = getInternal(bucket, key); - objects.add(object); - } catch (S3NotFoundException e) { - // do nothing - } - } - return objects; - }, () -> telemetry.get("GetObjects", bucket, null, null)); - } - - @Override - public List getMeta(String bucket, Collection keys) { - return wrapWithTelemetry(() -> { - final List metas = new ArrayList<>(keys.size()); - for (String key : keys) { - try { - S3ObjectMeta meta = getMeta(bucket, key); - metas.add(meta); - } catch (S3NotFoundException e) { - // do nothing - } - } - return metas; - }, () -> telemetry.get("GetObjectMetas", bucket, null, null)); - } - - @Override - public S3ObjectList list(String bucket, @Nullable String prefix, @Nullable String delimiter, int limit) { - return wrapWithTelemetry(() -> listInternal(bucket, prefix, delimiter, limit), - () -> telemetry.get("ListObjects", bucket, prefix, null)); - } - - private S3ObjectList listInternal(String bucket, @Nullable String prefix, @Nullable String delimiter, int limit) { - var metaList = listMetaInternal(bucket, prefix, delimiter, limit); - - final List objects = new ArrayList<>(metaList.metas().size()); - for (S3ObjectMeta meta : metaList.metas()) { - S3Object object = getInternal(bucket, meta.key()); - objects.add(object); - } - - return new MinioS3ObjectList(metaList, objects); - } - - @Override - public S3ObjectMetaList listMeta(String bucket, @Nullable String prefix, @Nullable String delimiter, int limit) { - return wrapWithTelemetry(() -> listMetaInternal(bucket, prefix, delimiter, limit), - () -> telemetry.get("ListObjectMetas", bucket, prefix, null)); - } - - private S3ObjectMetaList listMetaInternal(String bucket, @Nullable String prefix, @Nullable String delimiter, int limit) { - try { - var response = minioClient.listObjects(ListObjectsArgs.builder() - .bucket(bucket) - .prefix(prefix) - .maxKeys(limit) - .delimiter(delimiter) - .build()); - - final List metas = new ArrayList<>(); - for (Result result : response) { - metas.add(new MinioS3ObjectMeta(result.get())); - } - - return new MinioS3ObjectMetaList(prefix, metas); - } catch (Exception e) { - throw handleException(e); - } - } - - @Override - public List list(String bucket, Collection prefixes, @Nullable String delimiter, int limitPerPrefix) { - return wrapWithTelemetry(() -> { - final List lists = new ArrayList<>(prefixes.size()); - for (String prefix : prefixes) { - S3ObjectList list = listInternal(bucket, prefix, delimiter, limitPerPrefix); - lists.add(list); - } - return lists; - }, () -> telemetry.get("ListMultiObjects", bucket, null, null)); - } - - @Override - public List listMeta(String bucket, Collection prefixes, @Nullable String delimiter, int limitPerPrefix) { - return wrapWithTelemetry(() -> { - final List lists = new ArrayList<>(prefixes.size()); - for (String prefix : prefixes) { - var list = listMeta(bucket, prefix, delimiter, limitPerPrefix); - lists.add(list); - } - return lists; - }, () -> telemetry.get("ListMultiObjectMetas", bucket, null, null)); - } - - @Override - public S3ObjectUpload put(String bucket, String key, S3Body body) { - var requestBuilder = PutObjectArgs.builder() - .bucket(bucket) - .object(key) - .contentType(body.type() == null ? "application/octet-stream" : body.type()); - - if (body.size() > 0 && body.encoding() != null) { - requestBuilder.headers(Map.of( - "content-encoding", String.valueOf(body.encoding()), - "content-length", String.valueOf(body.size()) - )); - } else if (body.size() > 0) { - requestBuilder.headers(Map.of( - "content-length", String.valueOf(body.size()) - )); - } else if (body.encoding() != null) { - requestBuilder.headers(Map.of( - "content-encoding", String.valueOf(body.encoding()) - )); - } - - var ctx = Context.current(); - try { - var fork = ctx.fork(); - fork.inject(); - - var context = telemetry.get("PutObject", bucket, key, body.size() > 0 ? body.size() : null); - try { - if (body instanceof ByteS3Body bb) { - final ObjectWriteResponse response = minioClient.putObject(requestBuilder.stream(new BufferedInputStream(new ByteArrayInputStream(bb.bytes())), bb.size(), -1).build()); - return new MinioS3ObjectUpload(response.versionId()); - } else if (body.size() > 0) { - final ObjectWriteResponse response = minioClient.putObject(requestBuilder.stream(body.asInputStream(), body.size(), minioS3ClientConfig.upload().partSize().toBytes()).build()); - return new MinioS3ObjectUpload(response.versionId()); - } else { - final ObjectWriteResponse response = minioClient.putObject(requestBuilder.stream(body.asInputStream(), -1, minioS3ClientConfig.upload().partSize().toBytes()).build()); - return new MinioS3ObjectUpload(response.versionId()); - } - } catch (Exception e) { - S3Exception ex = handleException(e); - context.close(ex); - throw ex; - } - } finally { - ctx.inject(); - } - } - - @Override - public void delete(String bucket, String key) { - var ctx = Context.current(); - try { - var fork = ctx.fork(); - fork.inject(); - - var context = telemetry.get("DeleteObject", bucket, key, null); - try { - minioClient.removeObject(RemoveObjectArgs.builder() - .bucket(bucket) - .object(key) - .build()); - context.close(); - } catch (Exception e) { - S3Exception ex = handleException(e); - context.close(ex); - throw ex; - } - } finally { - ctx.inject(); - } - } - - @Override - public void delete(String bucket, Collection keys) { - var ctx = Context.current(); - try { - var fork = ctx.fork(); - fork.inject(); - - var context = telemetry.get("DeleteObjects", bucket, null, null); - try { - var response = minioClient.removeObjects(RemoveObjectsArgs.builder() - .bucket(bucket) - .objects(keys.stream() - .map(DeleteObject::new) - .toList()) - .build()); - - final List errors = new ArrayList<>(keys.size()); - for (Result result : response) { - DeleteError er = result.get(); - errors.add(new S3DeleteException.Error(er.objectName(), er.bucketName(), er.code(), er.message())); - } - - if (!errors.isEmpty()) { - throw new S3DeleteException(errors); - } - - context.close(); - } catch (Exception e) { - S3Exception ex = handleException(e); - context.close(ex); - throw ex; - } - } finally { - ctx.inject(); - } - } - - private static T wrapWithTelemetry(Supplier operationSupplier, - Supplier contextSupplier) { - var ctx = Context.current(); - try { - var fork = ctx.fork(); - fork.inject(); - - var context = contextSupplier.get(); - try { - T value = operationSupplier.get(); - context.close(); - return value; - } catch (Exception e) { - S3Exception ex = handleException(e); - context.close(ex); - throw ex; - } - } finally { - ctx.inject(); - } - } - - private static S3Exception handleException(Throwable e) { - Throwable cause = e; - if (e instanceof CompletionException ce) { - cause = ce.getCause(); - } - - if (cause instanceof S3Exception se) { - return se; - } else if (cause instanceof ErrorResponseException re) { - if ("NoSuchKey".equals(re.errorResponse().code())) { - return S3NotFoundException.ofNoSuchKey(cause, re.errorResponse().message()); - } else if ("NoSuchBucket".equals(re.errorResponse().code())) { - return S3NotFoundException.ofNoSuchBucket(cause, re.errorResponse().message()); - } else { - return new S3Exception(cause, re.errorResponse().code(), re.errorResponse().message()); - } - } else { - return new S3Exception(cause, cause.getClass().getSimpleName(), cause.getMessage()); - } - } -} diff --git a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3Object.java b/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3Object.java deleted file mode 100644 index 7f590c91d..000000000 --- a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3Object.java +++ /dev/null @@ -1,77 +0,0 @@ -package ru.tinkoff.kora.s3.client.minio; - -import io.minio.GetObjectResponse; -import okhttp3.Headers; -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.s3.client.model.S3Body; -import ru.tinkoff.kora.s3.client.model.S3Object; -import ru.tinkoff.kora.s3.client.model.S3ObjectMeta; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.time.Instant; -import java.util.Objects; - -@ApiStatus.Experimental -final class MinioS3Object implements S3Object, S3ObjectMeta { - - private final S3Body body; - private final String key; - private final Instant modified; - private final long size; - - public MinioS3Object(GetObjectResponse response) { - this.key = response.object(); - this.modified = response.headers().getInstant("Modified"); - try { - final Headers headers = response.headers(); - this.size = headers.get("Content-Length") == null - ? response.available() - : Long.valueOf(headers.get("Content-Length")); - this.body = new MinioS3Body(response, size, headers.get("Content-Encoding"), headers.get("Content-Type")); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - @Override - public String key() { - return key; - } - - @Override - public Instant modified() { - return modified; - } - - @Override - public long size() { - return size; - } - - @Override - public S3Body body() { - return body; - } - - @Override - public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; - MinioS3Object that = (MinioS3Object) object; - return size == that.size && Objects.equals(key, that.key); - } - - @Override - public int hashCode() { - return Objects.hash(key, size); - } - - @Override - public String toString() { - return "MinioS3Object{key=" + key + - ", size=" + size + - ", modified=" + modified + - '}'; - } -} diff --git a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectList.java b/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectList.java deleted file mode 100644 index afd2fbb66..000000000 --- a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectList.java +++ /dev/null @@ -1,41 +0,0 @@ -package ru.tinkoff.kora.s3.client.minio; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.s3.client.model.S3Object; -import ru.tinkoff.kora.s3.client.model.S3ObjectList; -import ru.tinkoff.kora.s3.client.model.S3ObjectMeta; -import ru.tinkoff.kora.s3.client.model.S3ObjectMetaList; - -import java.util.List; - -@ApiStatus.Experimental -final class MinioS3ObjectList implements S3ObjectList { - - private final String prefix; - private final List objects; - - public MinioS3ObjectList(S3ObjectMetaList metaList, List objects) { - this.objects = objects; - this.prefix = metaList.prefix(); - } - - @Override - public List objects() { - return objects; - } - - @Override - public String prefix() { - return prefix; - } - - @Override - public List metas() { - return (List) ((List) objects); - } - - @Override - public String toString() { - return objects.toString(); - } -} diff --git a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectMeta.java b/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectMeta.java deleted file mode 100644 index 53ff9d521..000000000 --- a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectMeta.java +++ /dev/null @@ -1,66 +0,0 @@ -package ru.tinkoff.kora.s3.client.minio; - -import io.minio.StatObjectResponse; -import io.minio.messages.Item; -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.s3.client.model.S3ObjectMeta; - -import java.time.Instant; -import java.util.Objects; - -@ApiStatus.Experimental -final class MinioS3ObjectMeta implements S3ObjectMeta { - - private final String key; - private final Instant modified; - private final long size; - - public MinioS3ObjectMeta(Item item) { - this.key = item.objectName(); - this.modified = item.lastModified().toInstant(); - this.size = item.size(); - } - - public MinioS3ObjectMeta(StatObjectResponse response) { - this.key = response.object(); - this.modified = response.lastModified().toInstant(); - this.size = response.size(); - response.contentType(); - } - - @Override - public String key() { - return key; - } - - @Override - public Instant modified() { - return modified; - } - - @Override - public long size() { - return size; - } - - @Override - public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; - MinioS3ObjectMeta that = (MinioS3ObjectMeta) object; - return size == that.size && Objects.equals(key, that.key); - } - - @Override - public int hashCode() { - return Objects.hash(key, size); - } - - @Override - public String toString() { - return "MinioS3ObjectMeta{key=" + key + - ", modified=" + modified + - ", size=" + size + - '}'; - } -} diff --git a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectMetaList.java b/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectMetaList.java deleted file mode 100644 index e9fe3b16a..000000000 --- a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectMetaList.java +++ /dev/null @@ -1,16 +0,0 @@ -package ru.tinkoff.kora.s3.client.minio; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.s3.client.model.S3ObjectMeta; -import ru.tinkoff.kora.s3.client.model.S3ObjectMetaList; - -import java.util.List; - -@ApiStatus.Experimental -record MinioS3ObjectMetaList(String prefix, List metas) implements S3ObjectMetaList { - - @Override - public String toString() { - return metas.toString(); - } -} diff --git a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectUpload.java b/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectUpload.java deleted file mode 100644 index 9252a5e1f..000000000 --- a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/MinioS3ObjectUpload.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.tinkoff.kora.s3.client.minio; - -import org.jetbrains.annotations.ApiStatus; -import ru.tinkoff.kora.s3.client.model.S3ObjectUpload; - -@ApiStatus.Experimental -record MinioS3ObjectUpload(String versionId) implements S3ObjectUpload { } diff --git a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/package-info.java b/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/package-info.java deleted file mode 100644 index 018ca6ae5..000000000 --- a/experimental/s3-client-minio/src/main/java/ru/tinkoff/kora/s3/client/minio/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -@Experimental -package ru.tinkoff.kora.s3.client.minio; - -import org.jetbrains.annotations.ApiStatus.Experimental; diff --git a/experimental/s3-client-symbol-processor/build.gradle b/experimental/s3-client-symbol-processor/build.gradle index 2b6f7514a..80d1c389a 100644 --- a/experimental/s3-client-symbol-processor/build.gradle +++ b/experimental/s3-client-symbol-processor/build.gradle @@ -1,36 +1,12 @@ -plugins { - id "java-test-fixtures" -} - apply from: "${project.rootDir}/gradle/kotlin-plugin.gradle" apply from: "${project.rootDir}/gradle/in-test-generated.gradle" dependencies { implementation project(":symbol-processor-common") - implementation libs.ksp.api - implementation libs.kotlin.reflect - implementation libs.kotlinpoet - implementation libs.kotlinpoet.ksp - - testImplementation project(":experimental:s3-client-aws") + testImplementation libs.mockito.kotlin + testImplementation project(":experimental:s3-client") testImplementation project(":internal:test-logging") testImplementation project(":config:config-common") testImplementation testFixtures(project(":symbol-processor-common")) - testImplementation(libs.kotlin.stdlib.lib) - testImplementation(libs.kotlin.coroutines.jdk8) -} - -kotlin { - sourceSets { - testGenerated { - kotlin.srcDir("build/generated/ksp/sources/kotlin") - } - } - sourceSets.main { - kotlin.srcDir("build/generated/ksp/main/kotlin") - } - sourceSets.test { - kotlin.srcDir("build/generated/ksp/test/kotlin") - } } diff --git a/experimental/s3-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ClassNames.kt b/experimental/s3-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ClassNames.kt new file mode 100644 index 000000000..c6bda2aac --- /dev/null +++ b/experimental/s3-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ClassNames.kt @@ -0,0 +1,37 @@ +package ru.tinkoff.kora.s3.client.symbol.processor + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.asTypeName +import java.io.InputStream + +object S3ClassNames { + object Annotation { + val client = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "Client") + val get = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "Get") + val list = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "List") + val put = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "Put") + val delete = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "Delete") + val s3OperationClassNames = setOf(get, list, put, delete) + + val bucket = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "Bucket") + + val listLimit = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "List", "Limit") + val listDelimiter = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "List", "Delimiter") + + } + + val client = ClassName("ru.tinkoff.kora.s3.client", "S3Client") + val clientFactory = ClassName("ru.tinkoff.kora.s3.client", "S3ClientFactory") + val body = ClassName("ru.tinkoff.kora.s3.client.model", "S3Body") + val s3Object = ClassName("ru.tinkoff.kora.s3.client.model", "S3Object") + val objectMeta = ClassName("ru.tinkoff.kora.s3.client.model", "S3ObjectMeta") + val uploadResult = ClassName("ru.tinkoff.kora.s3.client.model", "S3ObjectUploadResult") + val bodyTypes = setOf(body, InputStream::class.asTypeName(), ByteArray::class.asTypeName()) + + + val rangeData = ClassName("ru.tinkoff.kora.s3.client", "S3Client", "RangeData") + val rangeDataRange = ClassName("ru.tinkoff.kora.s3.client", "S3Client", "RangeData", "Range") + val rangeDataStartFrom = ClassName("ru.tinkoff.kora.s3.client", "S3Client", "RangeData", "StartFrom") + val rangeDataLastN = ClassName("ru.tinkoff.kora.s3.client", "S3Client", "RangeData", "LastN") + val rangeClasses = setOf(rangeData, rangeDataRange, rangeDataStartFrom, rangeDataLastN) +} diff --git a/experimental/s3-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ClientSymbolProcessor.kt b/experimental/s3-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ClientSymbolProcessor.kt index 869692a37..4216a11da 100644 --- a/experimental/s3-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ClientSymbolProcessor.kt +++ b/experimental/s3-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ClientSymbolProcessor.kt @@ -9,85 +9,29 @@ import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.writeTo -import ru.tinkoff.kora.common.Component -import ru.tinkoff.kora.common.Module import ru.tinkoff.kora.ksp.common.* import ru.tinkoff.kora.ksp.common.AnnotationUtils.findAnnotation -import ru.tinkoff.kora.ksp.common.AnnotationUtils.findValue import ru.tinkoff.kora.ksp.common.AnnotationUtils.findValueNoDefault +import ru.tinkoff.kora.ksp.common.AnnotationUtils.isAnnotationPresent import ru.tinkoff.kora.ksp.common.CommonAopUtils.overridingKeepAop import ru.tinkoff.kora.ksp.common.CommonClassNames.isCollection +import ru.tinkoff.kora.ksp.common.CommonClassNames.isList import ru.tinkoff.kora.ksp.common.CommonClassNames.isMap -import ru.tinkoff.kora.ksp.common.CommonClassNames.isVoid import ru.tinkoff.kora.ksp.common.FunctionUtils.isSuspend +import ru.tinkoff.kora.ksp.common.KotlinPoetUtils.controlFlow import ru.tinkoff.kora.ksp.common.KspCommonUtils.addOriginatingKSFile import ru.tinkoff.kora.ksp.common.KspCommonUtils.generated import ru.tinkoff.kora.ksp.common.KspCommonUtils.toTypeName -import ru.tinkoff.kora.ksp.common.TagUtils.addTag import ru.tinkoff.kora.ksp.common.exception.ProcessingErrorException import java.io.IOException -import java.nio.ByteBuffer -import java.util.concurrent.ExecutorService +import java.io.InputStream class S3ClientSymbolProcessor( private val environment: SymbolProcessorEnvironment ) : BaseSymbolProcessor(environment) { - companion object { - private val ANNOTATION_CLIENT: ClassName = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "Client") - private val MEMBER_AWAIT_FUTURE: MemberName = MemberName("kotlinx.coroutines.future", "await") - private val MEMBER_RUN_BLOCKING: MemberName = MemberName("kotlinx.coroutines", "runBlocking") - - private val ANNOTATION_OP_GET: ClassName = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "Get") - private val ANNOTATION_OP_LIST: ClassName = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "List") - private val ANNOTATION_OP_PUT: ClassName = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "Put") - private val ANNOTATION_OP_DELETE: ClassName = ClassName("ru.tinkoff.kora.s3.client.annotation", "S3", "Delete") - - private val CLASS_CONFIG = ClassName("ru.tinkoff.kora.s3.client", "S3Config") - private val CLASS_AWS_CONFIG = ClassName("ru.tinkoff.kora.s3.client.aws", "AwsS3ClientConfig") - private val CLASS_CLIENT_CONFIG: ClassName = ClassName("ru.tinkoff.kora.s3.client", "S3ClientConfig") - private val CLASS_CLIENT_SIMPLE_SYNC: ClassName = ClassName("ru.tinkoff.kora.s3.client", "S3KoraClient") - private val CLASS_CLIENT_SIMPLE_ASYNC: ClassName = ClassName("ru.tinkoff.kora.s3.client", "S3KoraAsyncClient") - private val CLASS_CLIENT_AWS_SYNC: ClassName = ClassName("software.amazon.awssdk.services.s3", "S3Client") - private val CLASS_CLIENT_AWS_ASYNC: ClassName = ClassName("software.amazon.awssdk.services.s3", "S3AsyncClient") - private val CLASS_CLIENT_AWS_ASYNC_MULTIPART = ClassName("software.amazon.awssdk.services.s3.internal.multipart", "MultipartS3AsyncClient") - private val CLASS_INTERCEPTOR_AWS_CONTEXT_KEY = ClassName("ru.tinkoff.kora.s3.client.aws", "AwsS3ClientTelemetryInterceptor") - private val CLASS_CLIENT_AWS_TAG = ClassName("software.amazon.awssdk.awscore", "AwsClient") - private val CLASS_CLIENT_AWS_MULTIPART_TAG = ClassName("software.amazon.awssdk.services.s3.model", "MultipartUpload") - - private val CLASS_S3_UPLOAD: ClassName = ClassName("ru.tinkoff.kora.s3.client.model", "S3ObjectUpload") - private val CLASS_S3_BODY: ClassName = ClassName("ru.tinkoff.kora.s3.client.model", "S3Body") - private val CLASS_S3_BODY_BYTES: ClassName = ClassName("ru.tinkoff.kora.s3.client.model", "ByteS3Body") - private val CLASS_S3_BODY_PUBLISHER: ClassName = ClassName("ru.tinkoff.kora.s3.client.model", "PublisherS3Body") - private val CLASS_S3_OBJECT: ClassName = ClassName("ru.tinkoff.kora.s3.client.model", "S3Object") - private val CLASS_S3_OBJECT_META: ClassName = ClassName("ru.tinkoff.kora.s3.client.model", "S3ObjectMeta") - private val CLASS_S3_OBJECT_MANY: TypeName = List::class.asTypeName().parameterizedBy(CLASS_S3_OBJECT) - private val CLASS_S3_OBJECT_META_MANY: TypeName = List::class.asTypeName().parameterizedBy(CLASS_S3_OBJECT_META) - private val CLASS_S3_OBJECT_LIST: ClassName = ClassName("ru.tinkoff.kora.s3.client.model", "S3ObjectList") - private val CLASS_S3_OBJECT_META_LIST: ClassName = ClassName("ru.tinkoff.kora.s3.client.model", "S3ObjectMetaList") - - private val CLASS_JDK_FLOW_ADAPTER = ClassName("reactor.adapter", "JdkFlowAdapter") - - private val CLASS_AWS_EXCEPTION_NO_KEY = ClassName("software.amazon.awssdk.services.s3.model", "NoSuchKeyException") - private val CLASS_AWS_EXCEPTION_NO_BUCKET = ClassName("software.amazon.awssdk.services.s3.model", "NoSuchBucketException") - private val CLASS_AWS_IS_SYNC_BODY: ClassName = ClassName("software.amazon.awssdk.core.sync", "RequestBody") - private val CLASS_AWS_IS_ASYNC_BODY: ClassName = ClassName("software.amazon.awssdk.core.async", "AsyncRequestBody") - private val CLASS_AWS_IS_ASYNC_TRANSFORMER: ClassName = ClassName("software.amazon.awssdk.core.async", "AsyncResponseTransformer") - private val CLASS_AWS_GET_REQUEST: ClassName = ClassName("software.amazon.awssdk.services.s3.model", "GetObjectRequest") - private val CLASS_AWS_GET_RESPONSE: ClassName = ClassName("software.amazon.awssdk.services.s3.model", "GetObjectResponse") - private val CLASS_AWS_GET_IS_RESPONSE: TypeName = ClassName("software.amazon.awssdk.core", "ResponseInputStream").parameterizedBy(CLASS_AWS_GET_RESPONSE) - private val CLASS_AWS_DELETE_REQUEST: ClassName = ClassName("software.amazon.awssdk.services.s3.model", "DeleteObjectRequest") - private val CLASS_AWS_DELETE_RESPONSE: ClassName = ClassName("software.amazon.awssdk.services.s3.model", "DeleteObjectResponse") - private val CLASS_AWS_DELETES_REQUEST: ClassName = ClassName("software.amazon.awssdk.services.s3.model", "DeleteObjectsRequest") - private val CLASS_AWS_DELETES_RESPONSE: ClassName = ClassName("software.amazon.awssdk.services.s3.model", "DeleteObjectsResponse") - private val CLASS_AWS_LIST_REQUEST: ClassName = ClassName("software.amazon.awssdk.services.s3.model", "ListObjectsV2Request") - private val CLASS_AWS_LIST_RESPONSE: ClassName = ClassName("software.amazon.awssdk.services.s3.model", "ListObjectsV2Response") - private val CLASS_AWS_PUT_REQUEST: ClassName = ClassName("software.amazon.awssdk.services.s3.model", "PutObjectRequest") - private val CLASS_AWS_PUT_RESPONSE: ClassName = ClassName("software.amazon.awssdk.services.s3.model", "PutObjectResponse") - } - override fun processRound(resolver: Resolver): List { - val symbols = resolver.getSymbolsWithAnnotation(ANNOTATION_CLIENT.canonicalName).toList() + val symbols = resolver.getSymbolsWithAnnotation(S3ClassNames.Annotation.client.canonicalName).toList() val symbolsToProcess = symbols.filter { it.validate() }.filterIsInstance() for (s3client in symbolsToProcess) { if (s3client.classKind != ClassKind.INTERFACE) { @@ -99,17 +43,17 @@ class S3ClientSymbolProcessor( val packageName = s3client.packageName.asString() try { - val typeSpec = generateClient(s3client, resolver) + val generatedConfig = generateClientConfig(s3client) + generatedConfig.typeSpec?.let { + FileSpec.get(packageName, it).writeTo(environment.codeGenerator, false) + } + + val typeSpec = generateClient(generatedConfig, s3client) val fileImplSpec = FileSpec.builder(packageName, typeSpec.name.toString()) .addType(typeSpec) .build() fileImplSpec.writeTo(codeGenerator = environment.codeGenerator, aggregating = false) - val configSpec = generateClientConfig(s3client) - val configImplSpec = FileSpec.builder(packageName, configSpec.name.toString()) - .addType(configSpec) - .build() - configImplSpec.writeTo(codeGenerator = environment.codeGenerator, aggregating = false) } catch (e: IOException) { throw IllegalStateException(e) } @@ -118,758 +62,402 @@ class S3ClientSymbolProcessor( return symbols.filterNot { it.validate() }.toList() } - private fun generateClient(s3client: KSClassDeclaration, resolver: Resolver): TypeSpec { - val implSpecBuilder: TypeSpec.Builder = TypeSpec.classBuilder(s3client.generatedClassName("Impl")) + private fun generateClient(generatedConfig: GenerateClientConfig, s3client: KSClassDeclaration): TypeSpec { + val implClassName = ClassName(s3client.packageName.asString(), s3client.generatedClassName("Impl")) + val implSpecBuilder: TypeSpec.Builder = TypeSpec.classBuilder(implClassName) .generated(S3ClientSymbolProcessor::class) - .addAnnotation(Component::class) .addSuperinterface(s3client.toTypeName()) + .addOriginatingKSFile(s3client) - val constructed = HashSet() + val clientAnnotation = s3client.findAnnotation(S3ClassNames.Annotation.client) val constructorBuilder = FunSpec.constructorBuilder() - val constructorCode = CodeBlock.builder() - implSpecBuilder.addProperty("_clientConfig", CLASS_CLIENT_CONFIG, KModifier.PRIVATE, KModifier.FINAL) - constructorCode.addStatement("this._clientConfig = clientConfig") - constructorBuilder.addParameter( - ParameterSpec.builder("clientConfig", CLASS_CLIENT_CONFIG) - .addAnnotation(s3client.toTypeName().makeTagAnnotationSpec()) - .build() - ) + val clientTag = clientAnnotation?.findValueNoDefault>("clientFactoryTag") + if (clientTag != null) { + constructorBuilder.addParameter(ParameterSpec.builder("clientFactory", S3ClassNames.clientFactory) + .addAnnotation(clientTag.makeTagAnnotationSpec()) + .build()) + } else { + constructorBuilder.addParameter("clientFactory", S3ClassNames.clientFactory) + } + generatedConfig.typeSpec?.let { configSpec -> + val configTypeName = ClassName(implClassName.packageName, configSpec.name!!) + constructorBuilder.addParameter("config", configTypeName) + implSpecBuilder.addProperty(PropertySpec.builder("config", configTypeName) + .initializer("config") + .build()) + } + implSpecBuilder.addProperty(PropertySpec.builder("client", S3ClassNames.client).initializer("clientFactory.create(%T::class.java)", implClassName).build()) + implSpecBuilder.primaryConstructor(constructorBuilder.build()) for (func in s3client.getDeclaredFunctions()) { - val operationType = getOperationType(func) - if (operationType == null) { - throw ProcessingErrorException("@S3.Client method without operation annotation can't be non default", func) - } else { - val operation = getOperation(func, operationType) - val methodSpec = func.overridingKeepAop(resolver) - .addCode(operation.code) - .build() - - implSpecBuilder.addFunction(methodSpec) - - val signatures = mutableListOf() - if (operation.impl == S3Operation.ImplType.SIMPLE) { - if (operation.mode == S3Operation.Mode.SYNC) { - signatures.add(Signature(CLASS_CLIENT_SIMPLE_SYNC, "simpleSyncClient")) - } else { - signatures.add(Signature(CLASS_CLIENT_SIMPLE_ASYNC, "simpleAsyncClient")) - } - } else if (operation.impl == S3Operation.ImplType.AWS) { - if (operation.mode == S3Operation.Mode.SYNC) { - signatures.add(Signature(CLASS_CLIENT_AWS_SYNC, "awsSyncClient")) - } else { - signatures.add(Signature(CLASS_CLIENT_AWS_ASYNC, "awsAsyncClient")) - } - if (operation.type == S3Operation.OperationType.PUT) { - signatures.add(Signature(CLASS_CLIENT_AWS_ASYNC_MULTIPART, "awsAsyncMultipartClient", listOf(CLASS_CLIENT_AWS_MULTIPART_TAG))) - signatures.add(Signature(CLASS_CLIENT_AWS_ASYNC, "awsAsyncClient")) - signatures.add(Signature(CLASS_AWS_CONFIG, "awsClientConfig")) - signatures.add(Signature(ExecutorService::class.asTypeName(), "awsAsyncExecutor", listOf(CLASS_CLIENT_AWS_TAG))) - } - } - - for (signature in signatures) { - if (!constructed.contains(signature)) { - if (signature.tags.isEmpty()) { - constructorBuilder.addParameter(signature.name, signature.type) - } else { - constructorBuilder.addParameter( - ParameterSpec.builder(signature.name, signature.type) - .addTag(signature.tags) - .build() - ) - } - implSpecBuilder.addProperty("_" + signature.name, signature.type, KModifier.PRIVATE, KModifier.FINAL) - constructorCode.addStatement("this._" + signature.name + " = " + signature.name) - constructed.add(signature) - } - } + if (func.isAbstract) { + val operation = generateMethod(generatedConfig, func) + implSpecBuilder.addFunction(operation) } } - constructorBuilder.addCode(constructorCode.build()) - implSpecBuilder.addFunction(constructorBuilder.build()) - return implSpecBuilder.build() } - private fun generateClientConfig(s3client: KSClassDeclaration): TypeSpec { - val clientAnnotation = s3client.findAnnotation(ANNOTATION_CLIENT) - val clientConfigPath = clientAnnotation!!.findValueNoDefault("value")!! + private data class GenerateClientConfig(val typeSpec: TypeSpec?, val paths: List) - val extractorClass = CommonClassNames.configValueExtractor.parameterizedBy(CLASS_CLIENT_CONFIG) - return TypeSpec.interfaceBuilder(s3client.generatedClass("ClientConfigModule")) + private fun generateClientConfig(s3client: KSClassDeclaration): GenerateClientConfig { + val bucketPaths = LinkedHashSet() + val onClass = s3client.findAnnotation(S3ClassNames.Annotation.bucket) + if (onClass != null) { + val value = onClass.findValueNoDefault("value") + if (value != null) { + bucketPaths.add(value) + } + } + for (func in s3client.getDeclaredFunctions()) { + val onMethod = func.findAnnotation(S3ClassNames.Annotation.bucket) + if (onMethod != null) { + val value = onMethod.findValueNoDefault("value") + if (value != null) { + bucketPaths.add(value) + } + } + } + if (bucketPaths.isEmpty()) { + return GenerateClientConfig(null, emptyList()) + } + val paths = ArrayList(bucketPaths) + val configType = ClassName(s3client.packageName.asString(), s3client.generatedClass("ClientConfig")) + val b = TypeSpec.classBuilder(configType) .generated(S3ClientSymbolProcessor::class) - .addAnnotation(AnnotationSpec.builder(Module::class).build()) .addOriginatingKSFile(s3client) - .addFunction( - FunSpec.builder("clientConfig") - .addModifiers(KModifier.PUBLIC) - .addAnnotation(s3client.toTypeName().makeTagAnnotationSpec()) - .addParameter(ParameterSpec.builder("config", CommonClassNames.config).build()) - .addParameter(ParameterSpec.builder("extractor", extractorClass).build()) - .addStatement("val value = config.get(%S)", clientConfigPath) - .addStatement( - "return extractor.extract(value) ?: throw %T.missingValueAfterParse(value)", - CommonClassNames.configValueExtractionException - ) - .returns(CLASS_CLIENT_CONFIG) - .build() - ) + val constructor = FunSpec.constructorBuilder() + .addParameter("config", CommonClassNames.config) .build() - } - - data class Signature(val type: TypeName, val name: String, val tags: List) { - - constructor(type: TypeName, name: String) : this(type, name, emptyList()) - } - - data class OperationMeta(val type: S3Operation.OperationType, val annotation: KSAnnotation) - - private fun getOperationType(method: KSFunctionDeclaration): OperationMeta? { - var value: OperationMeta? = null - - for (ksAnnotation in method.annotations) { - var type: S3Operation.OperationType? = null - if (ksAnnotation.annotationType.toTypeName() == ANNOTATION_OP_GET) { - type = S3Operation.OperationType.GET - } else if (ksAnnotation.annotationType.toTypeName() == ANNOTATION_OP_LIST) { - type = S3Operation.OperationType.LIST - } else if (ksAnnotation.annotationType.toTypeName() == ANNOTATION_OP_PUT) { - type = S3Operation.OperationType.PUT - } else if (ksAnnotation.annotationType.toTypeName() == ANNOTATION_OP_DELETE) { - type = S3Operation.OperationType.DELETE - } - - if (value == null && type != null) { - value = OperationMeta(type, ksAnnotation) - } else { - throw ProcessingErrorException("@S3.Client method must be annotated with single operation annotation", method) - } + for ((i, path) in paths.withIndex()) { + b.addProperty(PropertySpec.builder("bucket_$i", String::class).initializer("config.get(%S).asString()", path).build()) } - - return value + b.primaryConstructor(constructor) + return GenerateClientConfig(b.build(), paths) } - private fun getOperation(method: KSFunctionDeclaration, operationMeta: OperationMeta): S3Operation { - for (parameter in method.parameters) { - if (parameter.type.toTypeName().isNullable) { - throw ProcessingErrorException("S3.${operationMeta.type} operation can't have nullable method argument", method) + private fun generateMethod(generatedConfig: GenerateClientConfig, func: KSFunctionDeclaration): FunSpec { + if (func.isSuspend()) { + throw ProcessingErrorException("@S3.Client method can't be suspend", func) + } + for (parameter in func.parameters) { + if (parameter.type.resolve().isMarkedNullable) { + throw ProcessingErrorException("S3 operation can't have nullable method argument", parameter) } } - - val mode = if (method.isSuspend()) S3Operation.Mode.ASYNC else S3Operation.Mode.SYNC - - return if (S3Operation.OperationType.GET == operationMeta.type) { - operationGET(method, operationMeta, mode) - } else if (S3Operation.OperationType.LIST == operationMeta.type) { - operationLIST(method, operationMeta, mode) - } else if (S3Operation.OperationType.PUT == operationMeta.type) { - operationPUT(method, operationMeta, mode) - } else if (S3Operation.OperationType.DELETE == operationMeta.type) { - operationDELETE(method, operationMeta, mode) - } else { - throw UnsupportedOperationException("Unsupported S3 operation type") + val s3Operations = func.annotations + .filter { S3ClassNames.Annotation.s3OperationClassNames.contains(it.annotationType.toTypeName()) } + .toList() + if (s3Operations.isEmpty()) { + throw ProcessingErrorException("@S3.Client method must be annotated with single operation annotation", func) + } + if (s3Operations.size > 1) { + throw ProcessingErrorException("@S3.Client method without operation annotation can't be non default", func) + } + val operationAnnotation = s3Operations.first() + return when (s3Operations.first().annotationType.toTypeName()) { + S3ClassNames.Annotation.get -> operationGET(generatedConfig, func, operationAnnotation) + S3ClassNames.Annotation.delete -> operationDELETE(generatedConfig, func, operationAnnotation) + S3ClassNames.Annotation.list -> operationLIST(generatedConfig, func, operationAnnotation) + S3ClassNames.Annotation.put -> operationPUT(generatedConfig, func, operationAnnotation) + else -> throw IllegalStateException("Unknown operation annotation") } } - private fun operationGET(method: KSFunctionDeclaration, operationMeta: OperationMeta, mode: S3Operation.Mode): S3Operation { - val keyMapping: String? = operationMeta.annotation.findValueNoDefault("value") - val key: Key - val firstParameter = method.parameters.firstOrNull() - if (!keyMapping.isNullOrBlank()) { - key = parseKey(method, keyMapping) - if (key.params.isEmpty() && method.parameters.isNotEmpty()) { - throw ProcessingErrorException("@S3.Get operation key template must use method arguments or they should be removed", method) - } - } else if (method.parameters.size > 1) { - throw ProcessingErrorException("@S3.Get operation can't have multiple method parameters for keys without key template", method) - } else if (method.parameters.isEmpty()) { - throw ProcessingErrorException("@S3.Get operation must have key parameter", method) - } else { - key = Key(CodeBlock.of("val _key = %L.toString()\n", firstParameter!!.name!!.asString()), listOf(firstParameter)) + private fun operationGET(generatedConfig: GenerateClientConfig, func: KSFunctionDeclaration, annotation: KSAnnotation): FunSpec { + val returnType = func.returnType!!.resolve().toTypeName() + val allowedTypeNames = setOf( + S3ClassNames.objectMeta, + ByteArray::class.asTypeName(), + S3ClassNames.body, + S3ClassNames.s3Object, + InputStream::class.asTypeName() + ) + val returnTypeNonNullable = returnType.copy(false) + if (!allowedTypeNames.contains(returnTypeNonNullable)) { + throw ProcessingErrorException("Function ${func.simpleName.asString()} has return type $returnType, but should have one of $allowedTypeNames", func) } - - val returnType = method.returnType!!.toTypeName() - - if (CLASS_S3_OBJECT == returnType || CLASS_S3_OBJECT_META == returnType) { - if (firstParameter != null && firstParameter.type.resolve().isCollection()) { - throw ProcessingErrorException("@S3.Get operation expected single result, but parameter is collection of keys", method) - } - - val bodyBuilder: CodeBlock.Builder = CodeBlock.builder() - if (mode == S3Operation.Mode.SYNC) { - bodyBuilder.add("return _simpleSyncClient") - } else { - bodyBuilder.add("return _simpleAsyncClient") - } - - if (CLASS_S3_OBJECT == returnType) { - bodyBuilder.add(".get(_clientConfig.bucket(), _key)") - } else { - bodyBuilder.add(".getMeta(_clientConfig.bucket(), _key)") - } - - if (mode == S3Operation.Mode.ASYNC) { - bodyBuilder.add(".%M()", MEMBER_AWAIT_FUTURE) - } - - bodyBuilder.add("\n") - - val code: CodeBlock = CodeBlock.builder() - .add(key.code) - .add(bodyBuilder.build()) - .build() - - return S3Operation(method, operationMeta.annotation, S3Operation.OperationType.GET, S3Operation.ImplType.SIMPLE, mode, code) - } else if (CLASS_S3_OBJECT_MANY == returnType || CLASS_S3_OBJECT_META_MANY == returnType) { - if (firstParameter != null && !firstParameter.type.resolve().isCollection()) { - throw ProcessingErrorException("@S3.Get operation expected many results, but parameter isn't collection of keys", method) - } else if (!keyMapping.isNullOrBlank()) { - throw ProcessingErrorException("@S3.Get operation expected many results, key template can't be specified for collection of keys", method) - } - - val clientField = if (mode == S3Operation.Mode.SYNC) "_simpleSyncClient" else "_simpleAsyncClient" - - val bodyBuilder: CodeBlock.Builder = CodeBlock.builder() - if (CLASS_S3_OBJECT_MANY == returnType) { - bodyBuilder.add( - "return %L.get(_clientConfig.bucket(), %L)", - clientField, firstParameter!!.name!!.asString() - ) + val rangeParams = func.parameters.filter { S3ClassNames.rangeClasses.contains(it.type.toTypeName()) } + if (rangeParams.size > 1) { + throw ProcessingErrorException("Function ${func.simpleName.asString()} has more than one range parameter", rangeParams[1]) + } + val range = rangeParams.firstOrNull() + val bucket = extractBucket(generatedConfig, func) + val key = extractKey(func, annotation, true) + val builder = func.overridingKeepAop() + .addStatement("val _bucket = %L", bucket) + .addStatement("val _key = %L", key) + if (returnTypeNonNullable == S3ClassNames.objectMeta) { + if (range != null) { + throw ProcessingErrorException("Range parameters are not allowed on metadata requests", range) + } + if (returnType.isNullable) { + builder.addStatement("return this.client.getMetaOptional(_bucket, _key)") } else { - bodyBuilder.add( - "return %L.getMeta(_clientConfig.bucket(), %L)", - clientField, firstParameter!!.name!!.asString() - ) - } - - if (mode == S3Operation.Mode.ASYNC) { - bodyBuilder.add(".%M()", MEMBER_AWAIT_FUTURE) - } - - bodyBuilder.add("\n") - - return S3Operation(method, operationMeta.annotation, S3Operation.OperationType.GET, S3Operation.ImplType.SIMPLE, mode, bodyBuilder.build()) - } else if (CLASS_AWS_GET_RESPONSE == returnType || CLASS_AWS_GET_IS_RESPONSE == returnType) { - if (firstParameter != null && firstParameter.type.resolve().isCollection()) { - throw ProcessingErrorException("@S3.Get operation expected single result, but parameter is collection of keys", method) + builder.addStatement("return this.client.getMeta(_bucket, _key)!!") } - - val clientField = if (mode == S3Operation.Mode.SYNC) "_awsSyncClient" else "_awsAsyncClient" - - val codeBuilder: CodeBlock.Builder = CodeBlock.builder() - .add(key.code) - .add("\n") - .addStatement( - """ - var _request = %T.builder() - .bucket(_clientConfig.bucket()) - .key(_key) - .build() - """.trimIndent(), CLASS_AWS_GET_REQUEST - ) - .add("\n") - - if (mode == S3Operation.Mode.SYNC) { - if (CLASS_AWS_GET_RESPONSE == returnType) { - codeBuilder.addStatement("return %L.getObject(_request).response()", clientField).build() - } else { - codeBuilder.addStatement("return %L.getObject(_request)", clientField).build() + return builder.build() + } + if (range != null) { + builder.addStatement("val _range = %L", range) + } else { + builder.addStatement("val _range = null as %T?", S3ClassNames.rangeData) + } + if (returnTypeNonNullable == ByteArray::class.asTypeName()) { + if (returnType.isNullable) { + builder.controlFlow("this.client.getOptional(_bucket, _key, _range).use { _object ->") { + controlFlow("_object?.body().use { _body ->") { + addStatement("return _body?.asBytes()") + } } } else { - if (CLASS_AWS_GET_RESPONSE == returnType) { - codeBuilder - .add( - "return %L.getObject(_request, %T.toBlockingInputStream()).thenApply { it.response() }", - clientField, CLASS_AWS_IS_ASYNC_TRANSFORMER - ) - .build() - } else { - codeBuilder - .add( - "return %L.getObject(_request, %T.toBlockingInputStream())", - clientField, CLASS_AWS_IS_ASYNC_TRANSFORMER - ) - .build() + builder.controlFlow("this.client.get(_bucket, _key, _range).use { _object ->") { + controlFlow("_object.body().use { _body ->") { + addStatement("return _body.asBytes()!!") + } } - - codeBuilder.add(".%M()\n", MEMBER_AWAIT_FUTURE) - } - - return S3Operation(method, operationMeta.annotation, S3Operation.OperationType.GET, S3Operation.ImplType.AWS, mode, codeBuilder.build()) - } else { - if (firstParameter != null && firstParameter.type.resolve().isCollection()) { - throw ProcessingErrorException( - "@S3.Get operation unsupported method return signature, expected any of List<${CLASS_S3_OBJECT.simpleName}>/List<${CLASS_S3_OBJECT_META.simpleName}>", - method - ) - } else { - throw ProcessingErrorException( - "@S3.Get operation unsupported method return signature, expected any of ${CLASS_S3_OBJECT.simpleName}/${CLASS_S3_OBJECT_META.simpleName}/${CLASS_AWS_GET_RESPONSE.simpleName}/ResponseInputStream<${CLASS_AWS_GET_RESPONSE.simpleName}>", - method - ) } + return builder.build() } - } - - private fun operationLIST(method: KSFunctionDeclaration, operationMeta: OperationMeta, mode: S3Operation.Mode): S3Operation { - val keyMapping: String? = operationMeta.annotation.findValueNoDefault("value") - val key: Key? - val firstParameter = method.parameters.stream().findFirst().orElse(null) - if (!keyMapping.isNullOrBlank()) { - key = parseKey(method, keyMapping) - if (key.params.isEmpty() && method.parameters.isNotEmpty()) { - throw ProcessingErrorException("@S3.List operation prefix template must use method arguments or they should be removed", method) + if (returnTypeNonNullable == S3ClassNames.s3Object) { + if (returnType.isNullable) { + builder.addStatement("return this.client.getOptional(_bucket, _key, _range)") + } else { + builder.addStatement("return this.client.get(_bucket, _key, _range)!!") } - } else if (method.parameters.size > 1) { - throw ProcessingErrorException("@S3.List operation can't have multiple method parameters for keys without key template", method) - } else if (method.parameters.isEmpty()) { - key = null - } else if (firstParameter.type.resolve().isCollection()) { - throw ProcessingErrorException("@S3.List operation expected single result, but parameter is collection of keys", method) - } else { - key = Key(CodeBlock.of("val _key = %L.toString()", firstParameter.name!!.asString()), listOf(firstParameter)) + return builder.build() } - - val limit: Int = operationMeta.annotation.findValue("limit") ?: 1000 - val delimiter: String? = operationMeta.annotation.findValueNoDefault("delimiter") - val returnType: TypeName = method.returnType!!.toTypeName() - - if (CLASS_S3_OBJECT_LIST == returnType || CLASS_S3_OBJECT_META_LIST == returnType) { - val bodyBuilder: CodeBlock.Builder = CodeBlock.builder() - if (key != null) { - bodyBuilder.add(key.code).add("\n") - } - - if (mode == S3Operation.Mode.SYNC) { - bodyBuilder.add("return _simpleSyncClient") + if (returnTypeNonNullable == S3ClassNames.body) { + if (returnType.isNullable) { + builder.addStatement("return this.client.getOptional(_bucket, _key, _range)?.body()") } else { - bodyBuilder.add("return _simpleAsyncClient") + builder.addStatement("return this.client.get(_bucket, _key, _range).body()!!") } - - val keyField = if ((key == null)) "null as String?" else "_key" - if (CLASS_S3_OBJECT_LIST == returnType) { - bodyBuilder.add(".list(_clientConfig.bucket(), %L, %S, %L)", keyField, delimiter, limit) + return builder.build() + } + if (returnTypeNonNullable == InputStream::class.asClassName()) { + if (returnType.isNullable) { + builder.addStatement("return this.client.getOptional(_bucket, _key, _range)?.body()?.asInputStream()") } else { - bodyBuilder.add(".listMeta(_clientConfig.bucket(), %L, %S, %L)", keyField, delimiter, limit) - } - - if (mode == S3Operation.Mode.ASYNC) { - bodyBuilder.add(".%M()", MEMBER_AWAIT_FUTURE) - } - - bodyBuilder.add("\n") - - return S3Operation(method, operationMeta.annotation, S3Operation.OperationType.LIST, S3Operation.ImplType.SIMPLE, mode, bodyBuilder.build()) - } else if (CLASS_AWS_LIST_RESPONSE == returnType) { - val clientField = if (mode == S3Operation.Mode.SYNC) "_awsSyncClient" else "_awsAsyncClient" - val bodyBuilder: CodeBlock.Builder = CodeBlock.builder() - if (key != null) { - bodyBuilder.add(key.code).add("\n\n") - } - - val keyField = if ((key == null)) "null" else "_key" - bodyBuilder - .add( - """ - var _request = %L.builder() - .bucket(_clientConfig.bucket()) - .prefix(%L) - .delimiter(%S) - .maxKeys(%L) - .build() - """.trimIndent(), CLASS_AWS_LIST_REQUEST, keyField, delimiter, limit - ) - .add("\n") - .add("return %L.listObjectsV2(_request)", clientField) - - if (mode == S3Operation.Mode.ASYNC) { - bodyBuilder.add(".%M()", MEMBER_AWAIT_FUTURE) + builder.addStatement("return this.client.get(_bucket, _key, _range).body().asInputStream()!!") } - bodyBuilder.add("\n") + return builder.build() + } + throw IllegalStateException("Not gonna happen"); + } - return S3Operation(method, operationMeta.annotation, S3Operation.OperationType.LIST, S3Operation.ImplType.AWS, mode, bodyBuilder.build()) + private fun operationLIST(generatedConfig: GenerateClientConfig, func: KSFunctionDeclaration, annotation: KSAnnotation): FunSpec { + val returnType = func.returnType!!.resolve().toTypeName() + val isList = returnType == List::class.asClassName().parameterizedBy(S3ClassNames.objectMeta) + val isIterator = returnType == Iterator::class.asClassName().parameterizedBy(S3ClassNames.objectMeta) + if (!isList && !isIterator) { + throw ProcessingErrorException("Function ${func.simpleName.asString()} has return type $returnType, but should have one of List or Iterator", func) + } + val bucket = extractBucket(generatedConfig, func) + val key = extractKey(func, annotation, false) + val limit = extractLimit(func) + val delimiter = extractDelimiter(func) + + val builder = func.overridingKeepAop() + .addStatement("val _bucket = %L", bucket) + .addStatement("val _key = %L", key) + .addStatement("val _delimiter = %L", delimiter) + .addStatement("val _limit = %L", limit) + if (isList) { + return builder.addStatement("return this.client.list(_bucket, _key, _delimiter, _limit)").build() } else { - throw ProcessingErrorException( - "@S3.List operation unsupported method return signature, expected any of ${CLASS_S3_OBJECT_LIST.simpleName}/${CLASS_S3_OBJECT_META_LIST.simpleName}/${CLASS_AWS_LIST_RESPONSE.simpleName}", - method - ) + require(isIterator) + return builder.addStatement("return this.client.listIterator(_bucket, _key, _delimiter, _limit)").build() } } - private fun operationPUT(method: KSFunctionDeclaration, operationMeta: OperationMeta, mode: S3Operation.Mode): S3Operation { - val keyMapping: String? = operationMeta.annotation.findValueNoDefault("value") - val key: Key - - val keyParameters = method.parameters.stream() - .filter { p -> - val bodyType: TypeName = p.type.toTypeName() - (CLASS_S3_BODY != bodyType - && ByteBuffer::class.asTypeName() != bodyType - && ByteArray::class.asTypeName() != bodyType) + private fun extractDelimiter(func: KSFunctionDeclaration): CodeBlock { + val onParameter = func.parameters.filter { it.isAnnotationPresent(S3ClassNames.Annotation.listDelimiter) } + if (onParameter.size > 1) { + throw ProcessingErrorException("@S3.List operation expected at most one @S3.List.Delimiter parameter", onParameter[1]) + } + if (onParameter.isNotEmpty()) { + val parameter = onParameter[0] + val annotation = parameter.findAnnotation(S3ClassNames.Annotation.listDelimiter)!! + if (annotation.findValueNoDefault("value") != null) { + throw ProcessingErrorException("@S3.List.Delimiter annotation can't have value when annotating parameter", parameter) } - .toList() - - val firstParameter = keyParameters.firstOrNull() - if (!keyMapping.isNullOrBlank()) { - key = parseKey(method, keyMapping) - if (key.params.isEmpty() && keyParameters.isNotEmpty()) { - throw ProcessingErrorException("@S3.Put operation key template must use method arguments or they should be removed", method) + val parameterType = parameter.type.resolve().toTypeName() + if (parameterType == STRING) { + return CodeBlock.of("%N", parameter.name?.asString()) } - } else if (firstParameter == null) { - throw ProcessingErrorException("@S3.Put operation must have parameters", method) - } else if (firstParameter.type.resolve().isCollection()) { - throw ProcessingErrorException("@S3.Put operation expected single result, but parameter is collection of keys", method) - } else { - key = Key(CodeBlock.of("val _key = %L.toString()", firstParameter.name!!.asString()), java.util.List.of(firstParameter)) + throw ProcessingErrorException("@S3.List.Delimiter annotation can't have parameter of type $parameterType: only String is allowed", parameter); } - - val returnTypeMirror = method.returnType - val returnType: TypeName = returnTypeMirror!!.toTypeName() - - val bodyParam = method.parameters.stream() - .filter { p -> key.params.none { kp -> p === kp } } - .findFirst() - .orElseThrow { ProcessingErrorException("@S3.Put operation body parameter not found", method) } - - val bodyType: TypeName = bodyParam.type.toTypeName() - - val isResultUpload = CLASS_S3_UPLOAD == returnType - val bodyParamName = bodyParam.name!!.asString() - if (returnTypeMirror.isVoid() || isResultUpload) { - val bodyCode: CodeBlock - if (CLASS_S3_BODY == bodyType) { - bodyCode = CodeBlock.of("val _body = %L", bodyParamName) - } else { - val methodCall = when (bodyType) { - ByteBuffer::class.asTypeName() -> "ofBuffer" - ByteArray::class.asTypeName() -> "ofBytes" - else -> throw ProcessingErrorException("@S3.Put operation body must be S3Body/ByteArray/ByteBuffer", method) - } - - val type: String? = operationMeta.annotation.findValueNoDefault("type") - val encoding: String? = operationMeta.annotation.findValueNoDefault("encoding") - bodyCode = if (type != null && encoding != null) { - CodeBlock.of( - "val _body = %T.%L(%L, %S, %S)", - CLASS_S3_BODY, methodCall, bodyParamName, methodCall, type, encoding - ) - } else if (type != null) { - CodeBlock.of( - "val _body = %T.%L(%L, %S)", - CLASS_S3_BODY, methodCall, bodyParamName, methodCall, type - ) - } else if (encoding != null) { - CodeBlock.of( - "val _body = %T.%L(%L, null, %S)", - CLASS_S3_BODY, methodCall, bodyParamName, methodCall, encoding - ) - } else { - CodeBlock.of( - "val _body = %T.%L(%L)", - CLASS_S3_BODY, methodCall, bodyParamName - ) - } + val onMethod = func.findAnnotation(S3ClassNames.Annotation.listDelimiter) + if (onMethod != null) { + val value = onMethod.findValueNoDefault("value") + if (value != null) { + return CodeBlock.of("%S", value) } + throw ProcessingErrorException("@S3.List.Delimiter annotation must have value when annotating method", func) + } + return CodeBlock.of("null as String?") + } - val methodBuilder: CodeBlock.Builder = CodeBlock.builder() - if (mode == S3Operation.Mode.SYNC) { - if (isResultUpload) { - methodBuilder.add("return _simpleSyncClient.put(_clientConfig.bucket(), _key, _body)") - } else { - methodBuilder.add("_simpleSyncClient.put(_clientConfig.bucket(), _key, _body)") - } - methodBuilder.add("\n") - } else { - methodBuilder.add("return _simpleAsyncClient.put(_clientConfig.bucket(), _key, _body)") - if (returnTypeMirror.isVoid()) { - methodBuilder.add(".thenApply { }") - } - methodBuilder.add(".%M()", MEMBER_AWAIT_FUTURE) - methodBuilder.add("\n") + private fun extractLimit(func: KSFunctionDeclaration): Any { + val onParameter = func.parameters.filter { it.isAnnotationPresent(S3ClassNames.Annotation.listLimit) } + if (onParameter.size > 1) { + throw ProcessingErrorException("@S3.List operation expected at most one @S3.List.Limit parameter", onParameter[1]) + } + if (onParameter.isNotEmpty()) { + val parameter = onParameter[0] + val annotation = parameter.findAnnotation(S3ClassNames.Annotation.listLimit)!! + if (annotation.findValueNoDefault("value") != null) { + throw ProcessingErrorException("@S3.List.Limit annotation can't have value when annotating parameter", parameter) + } + val parameterType = parameter.type.toTypeName() + return when (parameterType) { + INT -> CodeBlock.of("%M(1000, %N)", MemberName("kotlin.math", "min"), parameter.name?.asString()) + LONG -> CodeBlock.of("%M(1000, %T.toIntExact(%N))", MemberName("kotlin.math", "min"), Math::class.asClassName(), parameter.name?.asString()) + else -> throw ProcessingErrorException("@S3.List.Limit annotation can only be used on Int or Long parameters", parameter) } - - val code: CodeBlock = CodeBlock.builder() - .add(key.code) - .add("\n") - .add(bodyCode) - .add("\n") - .add(methodBuilder.build()) - .build() - - return S3Operation(method, operationMeta.annotation, S3Operation.OperationType.PUT, S3Operation.ImplType.SIMPLE, mode, code) - } else if (CLASS_AWS_PUT_RESPONSE == returnType) { - val bodyCode: CodeBlock - val requestBuilder: CodeBlock.Builder = CodeBlock.builder() - val type: String? = operationMeta.annotation.findValueNoDefault("type") - val encoding: String? = operationMeta.annotation.findValueNoDefault("encoding") - - if (CLASS_S3_BODY == bodyType) { - bodyCode = if (mode == S3Operation.Mode.SYNC) { - CodeBlock.builder() - .beginControlFlow("val _requestBody = if (%L is %T)", bodyParamName, CLASS_S3_BODY_BYTES) - .add("%T.fromBytes(%L.bytes())\n", CLASS_AWS_IS_SYNC_BODY, bodyParamName) - .nextControlFlow("else if (%L.size() > 0)", bodyParamName) - .add( - "%T.fromContentProvider({ %L.asInputStream() }, %L.size(), %L.type())\n", - CLASS_AWS_IS_SYNC_BODY, - bodyParamName, - bodyParamName, - bodyParamName - ) - .nextControlFlow("else") - .add("%T.fromContentProvider({ %L.asInputStream() }, %L.type())\n", CLASS_AWS_IS_SYNC_BODY, bodyParamName, bodyParamName) - .endControlFlow() - .build() - } else { - CodeBlock.of( - """ - val _bodySize = if(%L.size() > 0) %L.size() else null - val _requestBody = if(%L is %T) - %T.fromBytes(%L.bytes()) - else if(%L is %T) - %T.fromPublisher(%T.flowPublisherToFlux(%L.asPublisher())) - else - %T.fromInputStream(%L.asInputStream(), _bodySize, _awsAsyncExecutor) - """.trimIndent(), - bodyParamName, bodyParamName, - bodyParamName, CLASS_S3_BODY_BYTES, - CLASS_AWS_IS_ASYNC_BODY, bodyParamName, - bodyParamName, CLASS_S3_BODY_PUBLISHER, - CLASS_AWS_IS_ASYNC_BODY, CLASS_JDK_FLOW_ADAPTER, bodyParamName, - CLASS_AWS_IS_ASYNC_BODY, bodyParamName - ) - } - - requestBuilder.addStatement("_requestBuilder.contentLength(if(%L.size() > 0) %L.size() else null)", bodyParamName, bodyParamName) - if (type != null) { - requestBuilder.addStatement("_requestBuilder.contentType(%S)", type) - } else { - requestBuilder.addStatement("_requestBuilder.contentType(%L.type())", bodyParamName) - } - if (encoding != null) { - requestBuilder.addStatement("_requestBuilder.contentEncoding(%S)", encoding) - } else { - requestBuilder.addStatement("_requestBuilder.contentEncoding(%L.encoding())", bodyParamName) - } - } else { - val awsBodyClass: ClassName = if (mode == S3Operation.Mode.SYNC) CLASS_AWS_IS_SYNC_BODY else CLASS_AWS_IS_ASYNC_BODY - when (bodyType) { - ByteBuffer::class.asTypeName() -> { - bodyCode = CodeBlock.of( - "val _requestBody = %T.fromByteBuffer(%L)", - awsBodyClass, bodyParamName - ) - requestBuilder.addStatement("_requestBuilder.contentLength(%L.remaining())", bodyParamName) - } - - ByteArray::class.asTypeName() -> { - bodyCode = CodeBlock.of( - "val _requestBody = %T.fromBytes(%L)", - awsBodyClass, bodyParamName - ) - requestBuilder.addStatement("_requestBuilder.contentLength(%L.length)", bodyParamName) - } - - else -> throw ProcessingErrorException("@S3.Put operation body must be S3Body/ByteArray/ByteBuffer", method) - } - - if (type != null) { - requestBuilder.addStatement("_requestBuilder.contentType(%S)", type) - } - if (encoding != null) { - requestBuilder.addStatement("_requestBuilder.contentEncoding(%S)", encoding) - } + } + val onFunction = func.findAnnotation(S3ClassNames.Annotation.listLimit) + if (onFunction != null) { + val value = onFunction.findValueNoDefault("value") + return when { + value == null -> throw ProcessingErrorException("@S3.List.Limit annotation must have value when annotating method", func) + value <= 0 -> throw ProcessingErrorException("@S3.List.Limit should be more than zero", func) + value > 1000 -> throw ProcessingErrorException("@S3.List.Limit should be less than 1000", func) + else -> CodeBlock.of("%L", value) } + } + return CodeBlock.of("1000") + } - val clientField = if (mode == S3Operation.Mode.SYNC) "_awsSyncClient" else "_awsAsyncClient" - val bodyBuilder = CodeBlock.builder() - .add(key.code) - .add("\n\n") - .addStatement( - """ - val _requestBuilder = %T.builder() - .bucket(_clientConfig.bucket()) - .key(_key) - """.trimIndent(), CLASS_AWS_PUT_REQUEST - ) - .add(requestBuilder.build()) - .addStatement("val _request = _requestBuilder.build()") - .add("\n") - - if (mode == S3Operation.Mode.SYNC) { - bodyBuilder - .beginControlFlow("if(%L is %T)", bodyParamName, CLASS_S3_BODY_PUBLISHER) - .addStatement( - "val _asyncBody = %T.fromPublisher(%T.flowPublisherToFlux(%L.asPublisher()))", - CLASS_AWS_IS_ASYNC_BODY, CLASS_JDK_FLOW_ADAPTER, bodyParamName - ) - .addStatement( - "return %M { _awsAsyncClient.putObject(_request, _asyncBody).%M() }", - MEMBER_RUN_BLOCKING, MEMBER_AWAIT_FUTURE - ) - .nextControlFlow( - "else if(%L.size() < 0 || %L.size() > _awsClientConfig.upload().partSize().toBytes())", - bodyParamName, bodyParamName - ) - .addStatement("val _bodySize = if(%L.size() > 0) %L.size() else null", bodyParamName, bodyParamName) - .addStatement( - "val _asyncBody = %T.fromInputStream(%L.asInputStream(), _bodySize, _awsAsyncExecutor)", - CLASS_AWS_IS_ASYNC_BODY, bodyParamName - ) - .addStatement( - "return %M { _awsAsyncMultipartClient.putObject(_request, _asyncBody).%M() }", - MEMBER_RUN_BLOCKING, MEMBER_AWAIT_FUTURE - ) - .endControlFlow() - .add("\n") - } + private fun operationPUT(generatedConfig: GenerateClientConfig, func: KSFunctionDeclaration, annotation: KSAnnotation): FunSpec { + val returnType = func.returnType!!.toTypeName() + if (returnType != UNIT && returnType != S3ClassNames.uploadResult) { + throw ProcessingErrorException("@S3.Put operation return type must be Unit or S3ObjectUploadResult", func) + } + val bodyParams = func.parameters.filter { p -> S3ClassNames.bodyTypes.contains(p.type.resolve().toTypeName()) } + if (bodyParams.size != 1) { + throw ProcessingErrorException("@S3.Put operation must have exactly one parameter of types S3Body, byte[] or InputStream", func) + } + val bodyParam = bodyParams[0] + val bodyParamType = bodyParam.type.resolve().toTypeName() + val bucket = extractBucket(generatedConfig, func) + val key = extractKey(func, annotation, true) + val b = func.overridingKeepAop() + .addStatement("val _bucket = %L", bucket) + .addStatement("val _key = %L", key) + when (bodyParamType) { + S3ClassNames.body -> b.addStatement("val _body = %N", bodyParam.name?.asString()!!) + ByteArray::class.asClassName() -> b.addStatement("val _body = %T.ofBytes(%N)", S3ClassNames.body, bodyParam.name!!.asString()) + InputStream::class.asClassName() -> b.addStatement("val _body = %T.ofInputStream(%N)", S3ClassNames.body, bodyParam.name!!.asString()) + else -> throw IllegalStateException("never gonna happen") + } + b.addStatement("return this.client.put(_bucket, _key, _body)") + return b.build() + } - bodyBuilder - .add(bodyCode) - .add("\n\n") - - if (mode == S3Operation.Mode.ASYNC) { - bodyBuilder.add( - """ - return if(%L.size() > 0 && %L.size() <= _awsClientConfig.upload().partSize().toBytes()) - _awsAsyncClient.putObject(_request, _requestBody).%M() - else - _awsAsyncMultipartClient.putObject(_request, _requestBody).%M() - """.trimIndent(), bodyParamName, bodyParamName, MEMBER_AWAIT_FUTURE, MEMBER_AWAIT_FUTURE - ) + private fun operationDELETE(generatedConfig: GenerateClientConfig, func: KSFunctionDeclaration, annotation: KSAnnotation): FunSpec { + val returnType = func.returnType!!.toTypeName() + if (returnType != UNIT) { + throw ProcessingErrorException("@S3.Delete operation unsupported method return signature, expected Unit, got $returnType", func) + } + val bucket = extractBucket(generatedConfig, func) + val nonBucketParams = func.parameters.filter { !it.isAnnotationPresent(S3ClassNames.Annotation.bucket) } + if (nonBucketParams.isEmpty()) { + throw ProcessingErrorException("@S3.Delete operation must have key related parameter", func) + } + val funSpec = func.overridingKeepAop() + .addStatement("val _bucket = %L", bucket) + val firstKeyParam = nonBucketParams.first() + val firstKeyParamType = firstKeyParam.type.resolve() + if (nonBucketParams.size == 1 && (firstKeyParamType.isCollection() || firstKeyParamType.isList())) { + val collectionType = firstKeyParamType.toTypeName() as ParameterizedTypeName + if (collectionType.typeArguments.first() == STRING) { + funSpec.addStatement("val _key = %L", firstKeyParam.name!!.asString()) } else { - bodyBuilder - .add("return %L.putObject(_request, _requestBody)", clientField) + funSpec.addStatement("val _key = %L.map{it.toString()}", firstKeyParam.name!!.asString()) } - bodyBuilder.add("\n") - - return S3Operation(method, operationMeta.annotation, S3Operation.OperationType.PUT, S3Operation.ImplType.AWS, mode, bodyBuilder.build()) } else { - throw ProcessingErrorException("@S3.Put operation unsupported method return signature, expected any of Unit/${CLASS_S3_UPLOAD.simpleName}/${CLASS_AWS_PUT_RESPONSE.simpleName}", method) + val key = extractKey(func, annotation, true) + funSpec.addStatement("val _key = %L", key) } + funSpec.addStatement("this.client.delete(_bucket, _key)") + return funSpec.build() } - private fun operationDELETE(method: KSFunctionDeclaration, operationMeta: OperationMeta, mode: S3Operation.Mode): S3Operation { - val keyMapping: String? = operationMeta.annotation.findValueNoDefault("value") - val key: Key - val firstParameter = method.parameters.stream().findFirst().orElse(null) + + private fun extractKey(func: KSFunctionDeclaration, annotation: KSAnnotation, isRequired: Boolean): CodeBlock { + val keyMapping = annotation.findValueNoDefault("value") + val parameters = func.parameters.filter { + val parameterTypeName = it.type.toTypeName() + return@filter !it.isAnnotationPresent(S3ClassNames.Annotation.bucket) + && !it.isAnnotationPresent(S3ClassNames.Annotation.listLimit) + && !it.isAnnotationPresent(S3ClassNames.Annotation.listDelimiter) + && !S3ClassNames.rangeClasses.contains(parameterTypeName) + && !S3ClassNames.bodyTypes.contains(parameterTypeName) + } if (!keyMapping.isNullOrBlank()) { - key = parseKey(method, keyMapping) - if (key.params.isEmpty() && method.parameters.isNotEmpty()) { - throw ProcessingErrorException("@S3.Delete operation key template must use method arguments or they should be removed", method) + val key = parseKey(func, parameters, keyMapping) + if (key.params.isEmpty() && parameters.isNotEmpty()) { + throw ProcessingErrorException("@S3 operation key template must use method arguments or the should be removed", parameters[0]) } - } else if (method.parameters.size > 1) { - throw ProcessingErrorException("@S3.Delete operation can't have multiple method parameters for keys without key template", method) - } else if (method.parameters.isEmpty()) { - throw ProcessingErrorException("@S3.Delete operation must have key parameter", method) - } else { - key = Key(CodeBlock.of("val _key = %L.toString()", firstParameter.toString()), java.util.List.of(firstParameter)) + return key.code } - - val returnTypeMirror = method.returnType - val returnType: TypeName = returnTypeMirror!!.toTypeName() - - val isFirstParamCollection = firstParameter != null && firstParameter.type.resolve().isCollection() - if (returnTypeMirror.isVoid()) { - val clientField = if (mode == S3Operation.Mode.SYNC) "_simpleSyncClient" else "_simpleAsyncClient" - val bodyBuilder = CodeBlock.builder() - - val keyArgName: String - if (isFirstParamCollection) { - keyArgName = firstParameter.name!!.asString() - } else { - bodyBuilder.add(key.code).add(";\n") - keyArgName = "_key" - } - - if (mode == S3Operation.Mode.ASYNC) { - bodyBuilder.add("return ") - } - bodyBuilder.add("%L.delete(_clientConfig.bucket(), %L)", clientField, keyArgName) - if (mode == S3Operation.Mode.ASYNC) { - bodyBuilder.add(".thenApply { }") - bodyBuilder.add(".%M()", MEMBER_AWAIT_FUTURE) - } - - bodyBuilder.add("\n") - - return S3Operation(method, operationMeta.annotation, S3Operation.OperationType.DELETE, S3Operation.ImplType.SIMPLE, mode, bodyBuilder.build()) - } else if (CLASS_AWS_DELETE_RESPONSE == returnType) { - if (isFirstParamCollection) { - throw ProcessingErrorException("@S3.Delete operation expected single result, but parameter is collection of keys", method) - } - - val clientField = if (mode == S3Operation.Mode.SYNC) "_awsSyncClient" else "_awsAsyncClient" - val bodyBuilder = CodeBlock.builder() - .add(key.code) - .add("\n") - .add( - """ - var _request = %T.builder() - .bucket(_clientConfig.bucket()) - .key(_key) - .build() - """.trimIndent(), CLASS_AWS_DELETE_REQUEST - ) - .add("\n") - .add("return %L.deleteObject(_request)", clientField) - - if (mode == S3Operation.Mode.ASYNC) { - bodyBuilder.add(".%M()", MEMBER_AWAIT_FUTURE) - } - - return S3Operation(method, operationMeta.annotation, S3Operation.OperationType.DELETE, S3Operation.ImplType.AWS, mode, bodyBuilder.build()) - } else if (CLASS_AWS_DELETES_RESPONSE == returnType) { - if (isFirstParamCollection) { - throw ProcessingErrorException("@S3.Delete operation multiple keys, but parameter is not collection of keys", method) - } - - val clientField = if (mode == S3Operation.Mode.SYNC) "_awsSyncClient" else "_awsAsyncClient" - val bodyBuilder = CodeBlock.builder() - .add( - """ - var _request = %T.builder() - .bucket(_clientConfig.bucket()) - .delete( - %T.builder() - .objects(%L.map { %T.builder().key(it).build() }.toList()) - .build() - ) - .build() - """.trimIndent(), CLASS_AWS_DELETES_REQUEST, - ClassName("software.amazon.awssdk.services.s3.model", "Delete"), - firstParameter.name!!.asString(), - ClassName("software.amazon.awssdk.services.s3.model", "ObjectIdentifier") - ) - .add("\n") - .add("return %L.deleteObjects(_request)", clientField) - - if (mode == S3Operation.Mode.ASYNC) { - bodyBuilder.add(".%M()", MEMBER_AWAIT_FUTURE) + if (parameters.size > 1) { + throw ProcessingErrorException("@S3 operation can't have multiple function parameters for keys without key template", func) + } + if (parameters.isEmpty()) { + if (isRequired) { + throw ProcessingErrorException("@S3 operation must have at least one method parameter for key", func) } + return CodeBlock.of("null as String?") + } + if (parameters.first().type.resolve().isCollection()) { + throw ProcessingErrorException("@%${annotation.shortName.asString()} operation expected single result, but parameter is collection of keys", func) + } + return CodeBlock.of("%N.toString()", parameters.first().name!!.asString()) + } - return S3Operation(method, operationMeta.annotation, S3Operation.OperationType.DELETE, S3Operation.ImplType.AWS, mode, bodyBuilder.build()) - } else { - throw ProcessingErrorException( - "@S3.Delete operation unsupported method return signature, expected any of Void/${CLASS_AWS_DELETE_RESPONSE.simpleName}/${CLASS_AWS_DELETES_RESPONSE.simpleName}", - method - ) + private fun extractBucket(generatedConfig: GenerateClientConfig, func: KSFunctionDeclaration): CodeBlock { + val onParameter = func.parameters.filter { it.isAnnotationPresent(S3ClassNames.Annotation.bucket) } + if (onParameter.size > 1) { + throw ProcessingErrorException("@S3.Delete operation can't have multiple @S3.Bucket annotations", func) + } + onParameter.firstOrNull()?.let { + return CodeBlock.of("%N", it.name?.asString()!!) + } + val onMethod = func.findAnnotation(S3ClassNames.Annotation.bucket) + if (onMethod != null) { + val value = onMethod.findValueNoDefault("value")!! + val i = generatedConfig.paths.indexOf(value) + if (i < 0) { + throw ProcessingErrorException("@S3 operation bucket annotation value must be one of ${generatedConfig.paths.joinToString()}", func) + } + return CodeBlock.of("this.config.bucket_%L", i) + } + val onClass = func.parentDeclaration?.findAnnotation(S3ClassNames.Annotation.bucket) + if (onClass != null) { + val value = onClass.findValueNoDefault("value")!! + val i = generatedConfig.paths.indexOf(value) + if (i < 0) { + throw ProcessingErrorException("@S3 operation bucket annotation value must be one of ${generatedConfig.paths.joinToString()}", func) + } + return CodeBlock.of("this.config.bucket_%L", i) } + throw ProcessingErrorException("S3 operation expected bucket on parameter, function or class but got none", func) } data class Key(val code: CodeBlock, val params: List) - private fun parseKey(method: KSFunctionDeclaration, keyTemplate: String): Key { + private fun parseKey(method: KSFunctionDeclaration, parameters: List, keyTemplate: String): Key { var indexStart = keyTemplate.indexOf("{") if (indexStart == -1) { - return Key(CodeBlock.of("val _key = %S\n", keyTemplate), listOf()) + return Key(CodeBlock.of("%S\n", keyTemplate), listOf()) } - val params: MutableList = ArrayList() + val params = ArrayList() val builder = CodeBlock.builder() - builder.add("val _key = ") var indexEnd = 0 while (indexStart != -1) { @@ -884,19 +472,13 @@ class S3ClientSymbolProcessor( indexEnd = keyTemplate.indexOf("}", indexStart) val paramName = keyTemplate.substring(indexStart + 1, indexEnd) - val parameter = method.parameters.stream() - .filter { p -> - val bodyType: TypeName = p.type.toTypeName() - (ByteBuffer::class.asClassName() != bodyType && ByteArray::class.asClassName() != bodyType) - } - .filter { p -> p.name!!.asString() == paramName } - .findFirst() - .orElseThrow { - ProcessingErrorException( - "@S3 operation key part named '${paramName}' expected, but wasn't found", - method - ) - } + val parameter = parameters.firstOrNull { p -> p.name!!.asString() == paramName } + if (parameter == null) { + throw ProcessingErrorException( + "@S3 operation key part named '${paramName}' expected, but wasn't found", + method + ) + } val paramType = parameter.type.resolve() if (paramType.isCollection() || paramType.isMap()) { @@ -915,7 +497,7 @@ class S3ClientSymbolProcessor( builder.add(" + %S", keyTemplate.substring(indexEnd + 1)) } - return Key(builder.add("\n").build(), params) + return Key(builder.build(), params) } } diff --git a/experimental/s3-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3Operation.kt b/experimental/s3-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3Operation.kt index ae0b0c5ee..d07a35269 100644 --- a/experimental/s3-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3Operation.kt +++ b/experimental/s3-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3Operation.kt @@ -7,26 +7,12 @@ import com.squareup.kotlinpoet.CodeBlock data class S3Operation( val method: KSFunctionDeclaration, val annotation: KSAnnotation, - val type: OperationType, - val impl: ImplType, - val mode: Mode, val code: CodeBlock ) { - enum class Mode { - ASYNC, - SYNC - } - enum class OperationType { GET, LIST, PUT, DELETE } - - enum class ImplType { - SIMPLE, - AWS, - MINIO - } } diff --git a/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/AbstractS3Test.kt b/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/AbstractS3Test.kt new file mode 100644 index 000000000..d05e620e4 --- /dev/null +++ b/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/AbstractS3Test.kt @@ -0,0 +1,33 @@ +package ru.tinkoff.kora.s3.client.symbol.processor + +import org.intellij.lang.annotations.Language +import org.mockito.Mockito +import ru.tinkoff.kora.aop.symbol.processor.AopSymbolProcessorProvider +import ru.tinkoff.kora.ksp.common.AbstractSymbolProcessorTest +import ru.tinkoff.kora.s3.client.S3Client +import ru.tinkoff.kora.s3.client.S3ClientFactory + +abstract class AbstractS3Test : AbstractSymbolProcessorTest() { + override fun commonImports(): String { + return super.commonImports() + """ + import java.nio.ByteBuffer; + import java.io.InputStream; + import ru.tinkoff.kora.s3.client.annotation.*; + import ru.tinkoff.kora.s3.client.annotation.S3.*; + import ru.tinkoff.kora.s3.client.model.*; + import ru.tinkoff.kora.s3.client.*; + import ru.tinkoff.kora.s3.client.S3Client.*; + """.trimIndent() + } + + protected var s3Client: S3Client = Mockito.mock(S3Client::class.java) + + protected fun compile(@Language("kotlin") source: String, vararg addArgs: Any?): TestObject { + val result = this.compile0(listOf(S3ClientSymbolProcessorProvider(), AopSymbolProcessorProvider()), source) + result.assertSuccess() + val args = ArrayList(1 + addArgs.size) + args.add(S3ClientFactory { s3Client }) + args.addAll(addArgs) + return newObject("\$Client_Impl", *args.toArray()) + } +} diff --git a/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3AwsSuspendClientTests.kt b/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3AwsSuspendClientTests.kt deleted file mode 100644 index 84135f4d6..000000000 --- a/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3AwsSuspendClientTests.kt +++ /dev/null @@ -1,160 +0,0 @@ -package ru.tinkoff.kora.s3.client.symbol.processor - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import ru.tinkoff.kora.ksp.common.AbstractSymbolProcessorTest - -class S3AwsSuspendClientTests : AbstractSymbolProcessorTest() { - - override fun commonImports(): String { - return super.commonImports() + """ - import java.util.List - import java.util.Collection - import ru.tinkoff.kora.s3.client.annotation.* - import ru.tinkoff.kora.s3.client.annotation.S3.* - import ru.tinkoff.kora.s3.client.model.* - import ru.tinkoff.kora.s3.client.* - import ru.tinkoff.kora.s3.client.model.S3Object - import software.amazon.awssdk.services.s3.model.* - """.trimIndent() - } - - @Test - fun clientGetAws() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Get - suspend fun get(key: String): GetObjectResponse - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientListAws() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List - suspend fun list(): ListObjectsV2Response - } - - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientListAwsWithPrefix() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List - suspend fun list(prefix: String): ListObjectsV2Response - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientListAwsLimit() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List(limit = 100) - suspend fun list(prefix: String): ListObjectsV2Response - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientListKeyAndDelimiter() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List(value = "some/path/to/{key1}/object", delimiter = "/") - suspend fun list(key1: String): ListObjectsV2Response - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientDeleteAws() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Delete - suspend fun delete(key: String): DeleteObjectResponse - } - - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientDeletesAws() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Delete - suspend fun delete(key: List): DeleteObjectsResponse - } - - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientPutBody() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Put - suspend fun put(key: String, value: S3Body): PutObjectResponse - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } -} diff --git a/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3KoraClientTests.kt b/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ClientTests.kt similarity index 94% rename from experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3KoraClientTests.kt rename to experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ClientTests.kt index c58ed803a..6183d7c68 100644 --- a/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3KoraClientTests.kt +++ b/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ClientTests.kt @@ -1,11 +1,10 @@ package ru.tinkoff.kora.s3.client.symbol.processor -import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import ru.tinkoff.kora.ksp.common.AbstractSymbolProcessorTest -class S3KoraClientTests : AbstractSymbolProcessorTest() { +class S3ClientTests : AbstractSymbolProcessorTest() { override fun commonImports(): String { return super.commonImports() + """ @@ -16,7 +15,6 @@ class S3KoraClientTests : AbstractSymbolProcessorTest() { import ru.tinkoff.kora.s3.client.model.*; import ru.tinkoff.kora.s3.client.*; import ru.tinkoff.kora.s3.client.model.S3Object; - import software.amazon.awssdk.services.s3.model.*; """.trimIndent() } @@ -72,23 +70,6 @@ class S3KoraClientTests : AbstractSymbolProcessorTest() { assertThat(clazz).isNotNull() } - @Test - fun clientGetManyObjects() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Get - fun get(key: List): List - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - @Test fun clientGetKeyConcat() { this.compile0( @@ -196,7 +177,7 @@ class S3KoraClientTests : AbstractSymbolProcessorTest() { interface Client { @S3.List - fun list(prefix: String): S3ObjectMetaList + fun list(prefix: String): List } """.trimIndent() ) @@ -213,7 +194,7 @@ class S3KoraClientTests : AbstractSymbolProcessorTest() { interface Client { @S3.List - fun list(prefix: String): S3ObjectList + fun list(prefix: String): List } """.trimIndent() ) @@ -230,7 +211,7 @@ class S3KoraClientTests : AbstractSymbolProcessorTest() { interface Client { @S3.List(limit = 100) - fun list(prefix: String): S3ObjectList + fun list(prefix: String): List } """.trimIndent() ) @@ -247,7 +228,7 @@ class S3KoraClientTests : AbstractSymbolProcessorTest() { interface Client { @S3.List("{key1}-{key2}") - fun list(key1: String, key2: Long): S3ObjectList + fun list(key1: String, key2: Long): List } """.trimIndent() ) @@ -296,7 +277,7 @@ class S3KoraClientTests : AbstractSymbolProcessorTest() { interface Client { @S3.List("const-key") - fun list(): S3ObjectList + fun list(): List } """.trimIndent() ) diff --git a/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3DeleteTest.kt b/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3DeleteTest.kt new file mode 100644 index 000000000..6d850fd0f --- /dev/null +++ b/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3DeleteTest.kt @@ -0,0 +1,60 @@ +package ru.tinkoff.kora.s3.client.symbol.processor + +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import java.util.* + +class S3DeleteTest : AbstractS3Test() { + @Test + fun testDeleteKey() { + val client = this.compile(""" + @S3.Client(clientFactoryTag = [String::class]) + interface Client { + @S3.Delete + fun deleteKey(@S3.Bucket bucket: String, key: String) + } + """.trimIndent()) + + val key1 = UUID.randomUUID().toString() + client.invoke("deleteKey", "bucket", key1) + Mockito.verify(s3Client).delete("bucket", key1) + Mockito.reset(s3Client) + } + + @Test + fun testDeleteKeyWithTemplate() { + val client = this.compile(""" + @S3.Client(clientFactoryTag = [String::class]) + interface Client { + @S3.Delete("some/prefix/{key1}/{key2}") + fun deleteKey(@S3.Bucket bucket: String, key1: String, key2: String) + } + """.trimIndent()) + + val key1 = UUID.randomUUID().toString() + val key2 = UUID.randomUUID().toString() + client.invoke("deleteKey", "bucket", key1, key2) + Mockito.verify(s3Client).delete("bucket", "some/prefix/${key1}/${key2}") + Mockito.reset(s3Client) + } + + @Test + fun testDeleteKeys() { + val client = this.compile(""" + @S3.Client(clientFactoryTag = [String::class]) + interface Client { + @S3.Delete("some/prefix/{key1}/{key2}") + fun deleteKeys(@S3.Bucket bucket: String, key: List) + + @S3.Delete("some/prefix/{key1}/{key2}") + fun deleteKeysNonString(@S3.Bucket bucket: String, key: List) + } + """.trimIndent()) + + val key1 = UUID.randomUUID().toString() + val key2 = UUID.randomUUID().toString() + client.invoke("deleteKeys", "bucket", listOf(key1, key2)) + Mockito.verify(s3Client).delete("bucket", listOf(key1, key2)) + Mockito.reset(s3Client) + } +} diff --git a/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3GetTest.kt b/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3GetTest.kt new file mode 100644 index 000000000..264812c49 --- /dev/null +++ b/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3GetTest.kt @@ -0,0 +1,370 @@ +package ru.tinkoff.kora.s3.client.symbol.processor + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import ru.tinkoff.kora.config.common.factory.MapConfigFactory +import ru.tinkoff.kora.s3.client.S3Client +import ru.tinkoff.kora.s3.client.model.S3Body +import ru.tinkoff.kora.s3.client.model.S3Object +import ru.tinkoff.kora.s3.client.model.S3ObjectMeta +import java.io.InputStream +import java.time.Instant +import java.util.* + +class S3GetTest : AbstractS3Test() { + @Test + fun testGetMetadata() { + val client = this.compile(""" + @S3.Client + interface Client { + @S3.Get + fun getRequired(@S3.Bucket bucket: String, key: String): S3ObjectMeta + + @S3.Get + fun getOptional(@S3.Bucket bucket: String, key: String): S3ObjectMeta? + } + """.trimIndent()) + + val meta = S3ObjectMeta("bucket", "key", Instant.now(), -1) + + whenever(s3Client.getMeta("bucket", "key")).thenReturn(meta) + assertThat(client.invoke("getRequired", "bucket", "key")).isSameAs(meta) + verify(s3Client).getMeta("bucket", "key") + reset(s3Client) + + whenever(s3Client.getMetaOptional("bucket", "key")).thenReturn(meta) + assertThat(client.invoke("getOptional", "bucket", "key")).isEqualTo(meta) + verify(s3Client).getMetaOptional("bucket", "key") + reset(s3Client) + + whenever(s3Client.getMetaOptional("bucket", "key")).thenReturn(null) + assertThat(client.invoke("getOptional", "bucket", "key")).isNull() + verify(s3Client).getMetaOptional("bucket", "key") + reset(s3Client) + } + + @Test + fun testGetBytes() { + val client = this.compile(""" + @S3.Client + interface Client { + @S3.Get + fun get(@S3.Bucket bucket: String, key: String): ByteArray + @S3.Get + fun getRange(@S3.Bucket bucket: String, key: String, range: RangeData): ByteArray + @S3.Get + fun getNullable(@S3.Bucket bucket: String, key: String): ByteArray? + } + """.trimIndent()) + + val meta = S3ObjectMeta("bucket", "key", Instant.now(), -1) + val bytes = UUID.randomUUID().toString().toByteArray() + val supplier: () -> S3Object = { S3Object(meta, S3Body.ofInputStream(bytes.inputStream(), bytes.size.toLong())) } + + whenever(s3Client.get("bucket", "key", null)).thenReturn(supplier()) + assertThat(client.invoke("get", "bucket", "key")).isEqualTo(bytes) + verify(s3Client).get("bucket", "key", null) + reset(s3Client) + + whenever(s3Client.get("bucket", "key", S3Client.RangeData.LastN(10))).thenReturn(supplier()) + assertThat(client.invoke("getRange", "bucket", "key", S3Client.RangeData.LastN(10))).isEqualTo(bytes) + verify(s3Client).get("bucket", "key", S3Client.RangeData.LastN(10)) + reset(s3Client) + + whenever(s3Client.getOptional("bucket", "key", null)).thenReturn(null) + assertThat(client.invoke("getNullable", "bucket", "key")).isNull() + verify(s3Client).getOptional("bucket", "key", null) + reset(s3Client) + + whenever(s3Client.getOptional("bucket", "key", null)).thenReturn(supplier()) + assertThat(client.invoke("getNullable", "bucket", "key")).isEqualTo(bytes) + verify(s3Client).getOptional("bucket", "key", null) + reset(s3Client) + } + + @Test + fun testGetS3Object() { + val client = this.compile(""" + @S3.Client + interface Client { + @S3.Get + fun get(@S3.Bucket bucket: String, key: String): S3Object + @S3.Get + fun getRange(@S3.Bucket bucket: String, key: String, range: RangeData): S3Object + @S3.Get + fun getNullable(@S3.Bucket bucket: String, key: String): S3Object? + } + """.trimIndent()) + + val meta = S3ObjectMeta("bucket", "key", Instant.now(), -1) + val bytes = UUID.randomUUID().toString().toByteArray() + val supplier: () -> S3Object = { S3Object(meta, S3Body.ofInputStream(bytes.inputStream(), bytes.size.toLong())) } + + whenever(s3Client.get("bucket", "key", null)).thenReturn(supplier()) + client.invoke("get", "bucket", "key").use { + assertThat(it).isNotNull() + assertThat(it.meta).isSameAs(meta) + it.body.use { + assertThat(it.asBytes()).isEqualTo(bytes) + } + } + verify(s3Client).get("bucket", "key", null) + reset(s3Client) + + whenever(s3Client.get("bucket", "key", S3Client.RangeData.LastN(10))).thenReturn(supplier()) + client.invoke("getRange", "bucket", "key", S3Client.RangeData.LastN(10)).use { + assertThat(it).isNotNull() + assertThat(it.meta).isSameAs(meta) + it.body.use { + assertThat(it.asBytes()).isEqualTo(bytes) + } + } + verify(s3Client).get("bucket", "key", S3Client.RangeData.LastN(10)) + reset(s3Client) + + whenever(s3Client.getOptional("bucket", "key", null)).thenReturn(supplier()) + client.invoke("getNullable", "bucket", "key").use { + assertThat(it).isNotNull() + assertThat(it.meta).isSameAs(meta) + it.body.use { + assertThat(it.asBytes()).isEqualTo(bytes) + } + } + verify(s3Client).getOptional("bucket", "key", null) + reset(s3Client) + + whenever(s3Client.getOptional("bucket", "key", null)).thenReturn(null) + client.invoke("getNullable", "bucket", "key").use { + assertThat(it).isNull() + } + verify(s3Client).getOptional("bucket", "key", null) + reset(s3Client) + } + + @Test + fun testGetS3Body() { + val client = this.compile(""" + @S3.Client + interface Client { + @S3.Get + fun get(@S3.Bucket bucket: String, key: String): S3Body + @S3.Get + fun getRange(@S3.Bucket bucket: String, key: String, range: RangeData): S3Body + @S3.Get + fun getNullable(@S3.Bucket bucket: String, key: String): S3Body? + } + """.trimIndent()) + + val meta = S3ObjectMeta("bucket", "key", Instant.now(), -1) + val bytes = UUID.randomUUID().toString().toByteArray() + val supplier: () -> S3Object = { S3Object(meta, S3Body.ofInputStream(bytes.inputStream(), bytes.size.toLong())) } + + whenever(s3Client.get("bucket", "key", null)).thenReturn(supplier()) + client.invoke("get", "bucket", "key").use { + assertThat(it).isNotNull() + assertThat(it.asBytes()).isEqualTo(bytes) + } + verify(s3Client).get("bucket", "key", null) + reset(s3Client) + + whenever(s3Client.get("bucket", "key", S3Client.RangeData.LastN(10))).thenReturn(supplier()) + client.invoke("getRange", "bucket", "key", S3Client.RangeData.LastN(10)).use { + assertThat(it).isNotNull() + assertThat(it.asBytes()).isEqualTo(bytes) + } + verify(s3Client).get("bucket", "key", S3Client.RangeData.LastN(10)) + reset(s3Client) + + whenever(s3Client.getOptional("bucket", "key", null)).thenReturn(supplier()) + client.invoke("getNullable", "bucket", "key").use { + assertThat(it).isNotNull() + assertThat(it.asBytes()).isEqualTo(bytes) + } + verify(s3Client).getOptional("bucket", "key", null) + reset(s3Client) + + whenever(s3Client.getOptional("bucket", "key", null)).thenReturn(null) + client.invoke("getNullable", "bucket", "key").use { + assertThat(it).isNull() + } + verify(s3Client).getOptional("bucket", "key", null) + reset(s3Client) + } + + @Test + fun testGetInputStream() { + val client = this.compile(""" + @S3.Client + interface Client { + @S3.Get + fun get(@S3.Bucket bucket: String, key: String): InputStream + @S3.Get + fun getRange(@S3.Bucket bucket: String, key: String, range: RangeData): InputStream + @S3.Get + fun getNullable(@S3.Bucket bucket: String, key: String): InputStream? + } + """.trimIndent()) + + val meta = S3ObjectMeta("bucket", "key", Instant.now(), -1) + val bytes = UUID.randomUUID().toString().toByteArray() + val supplier: () -> S3Object = { S3Object(meta, S3Body.ofInputStream(bytes.inputStream(), bytes.size.toLong())) } + + whenever(s3Client.get("bucket", "key", null)).thenReturn(supplier()) + client.invoke("get", "bucket", "key").use { + assertThat(it).isNotNull() + assertThat(it.readAllBytes()).isEqualTo(bytes) + } + verify(s3Client).get("bucket", "key", null) + reset(s3Client) + + whenever(s3Client.get("bucket", "key", S3Client.RangeData.LastN(10))).thenReturn(supplier()) + client.invoke("getRange", "bucket", "key", S3Client.RangeData.LastN(10)).use { + assertThat(it).isNotNull() + assertThat(it.readAllBytes()).isEqualTo(bytes) + } + verify(s3Client).get("bucket", "key", S3Client.RangeData.LastN(10)) + reset(s3Client) + + whenever(s3Client.getOptional("bucket", "key", null)).thenReturn(supplier()) + client.invoke("getNullable", "bucket", "key").use { + assertThat(it).isNotNull() + assertThat(it.readAllBytes()).isEqualTo(bytes) + } + verify(s3Client).getOptional("bucket", "key", null) + reset(s3Client) + + whenever(s3Client.getOptional("bucket", "key", null)).thenReturn(null) + client.invoke("getNullable", "bucket", "key").use { + assertThat(it).isNull() + } + verify(s3Client).getOptional("bucket", "key", null) + reset(s3Client) + } + + @Test + fun testGetWithKeyTemplate() { + val client = this.compile(""" + @S3.Client + interface Client { + @S3.Get("prefix/{key1}/{key2}/suffix") + fun get(@S3.Bucket bucket: String, key1: String, key2: String): ByteArray + } + """.trimIndent()) + + val k1 = UUID.randomUUID().toString() + val k2 = UUID.randomUUID().toString() + val key = "prefix/$k1/$k2/suffix" + val meta = S3ObjectMeta("bucket", key, Instant.now(), -1) + val bytes = UUID.randomUUID().toString().toByteArray() + val supplier: () -> S3Object = { S3Object(meta, S3Body.ofInputStream(bytes.inputStream(), bytes.size.toLong())) } + + whenever(s3Client.get("bucket", key, null)).thenReturn(supplier()) + client.invoke("get", "bucket", k1, k2).let { + assertThat(it).isNotNull() + assertThat(it).isEqualTo(bytes) + } + } + + @Test + fun testGetWithBucketOnClient() { + val config = MapConfigFactory.fromMap(mapOf( + "s3" to mapOf( + "client" to mapOf( + "bucket" to "on-class" + ) + ) + )) + val client = this.compile(""" + @S3.Client + @Bucket("s3.client.bucket") + interface Client { + @S3.Get("prefix/{key}/suffix") + fun get(key: String): ByteArray + } + """.trimIndent(), newGenerated("\$Client_ClientConfig", config)) + + val k1 = UUID.randomUUID().toString() + val key = "prefix/$k1/suffix" + val meta = S3ObjectMeta("bucket", key, Instant.now(), -1) + val bytes = UUID.randomUUID().toString().toByteArray() + val supplier: () -> S3Object = { S3Object(meta, S3Body.ofInputStream(bytes.inputStream(), bytes.size.toLong())) } + + whenever(s3Client.get("on-class", key, null)).thenReturn(supplier()) + client.invoke("get", k1).let { + assertThat(it).isNotNull() + assertThat(it).isEqualTo(bytes) + } + } + + @Test + fun testGetWithBucketOnMethod() { + val config = MapConfigFactory.fromMap(mapOf( + "s3" to mapOf( + "client" to mapOf( + "get" to mapOf( + "bucket" to "on-method" + ) + ) + ) + )) + val client = this.compile(""" + @S3.Client + interface Client { + @S3.Get("prefix/{key}/suffix") + @Bucket("s3.client.get.bucket") + fun get(key: String): ByteArray + } + """.trimIndent(), newGenerated("\$Client_ClientConfig", config)) + + val k1 = UUID.randomUUID().toString() + val key = "prefix/$k1/suffix" + val meta = S3ObjectMeta("bucket", key, Instant.now(), -1) + val bytes = UUID.randomUUID().toString().toByteArray() + val supplier: () -> S3Object = { S3Object(meta, S3Body.ofInputStream(bytes.inputStream(), bytes.size.toLong())) } + + whenever(s3Client.get("on-method", key, null)).thenReturn(supplier()) + client.invoke("get", k1).let { + assertThat(it).isNotNull() + assertThat(it).isEqualTo(bytes) + } + } + + @Test + fun testGetWithBucketOnMethodAndType() { + val config = MapConfigFactory.fromMap(mapOf( + "s3" to mapOf( + "client" to mapOf( + "bucket" to "on-class", + "get" to mapOf( + "bucket" to "on-method" + ) + ) + ) + )) + val client = this.compile(""" + @S3.Client + @Bucket("s3.client.bucket") + interface Client { + @S3.Get("prefix/{key}/suffix") + @Bucket("s3.client.get.bucket") + fun get(key: String): ByteArray + } + """.trimIndent(), newGenerated("\$Client_ClientConfig", config)) + + val k1 = UUID.randomUUID().toString() + val key = "prefix/$k1/suffix" + val meta = S3ObjectMeta("bucket", key, Instant.now(), -1) + val bytes = UUID.randomUUID().toString().toByteArray() + val supplier: () -> S3Object = { S3Object(meta, S3Body.ofInputStream(bytes.inputStream(), bytes.size.toLong())) } + + whenever(s3Client.get("on-method", key, null)).thenReturn(supplier()) + client.invoke("get", k1).let { + assertThat(it).isNotNull() + assertThat(it).isEqualTo(bytes) + } + } + +} diff --git a/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3KoraSuspendClientTests.kt b/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3KoraSuspendClientTests.kt deleted file mode 100644 index 9cacf265f..000000000 --- a/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3KoraSuspendClientTests.kt +++ /dev/null @@ -1,608 +0,0 @@ -package ru.tinkoff.kora.s3.client.symbol.processor - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import ru.tinkoff.kora.ksp.common.AbstractSymbolProcessorTest - -class S3KoraSuspendClientTests : AbstractSymbolProcessorTest() { - - override fun commonImports(): String { - return super.commonImports() + """ - import java.nio.ByteBuffer; - import java.io.InputStream; - import ru.tinkoff.kora.s3.client.annotation.*; - import ru.tinkoff.kora.s3.client.annotation.S3.*; - import ru.tinkoff.kora.s3.client.model.*; - import ru.tinkoff.kora.s3.client.*; - import ru.tinkoff.kora.s3.client.model.S3Object; - import software.amazon.awssdk.services.s3.model.*; - """.trimIndent() - } - - @Test - fun clientConfig() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Get - suspend fun get(key: String): S3ObjectMeta - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - - val config = compileResult.loadClass("\$Client_ClientConfigModule") - assertThat(config).isNotNull() - } - - // Get - @Test - fun clientGetMeta() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Get - suspend fun get(key: String): S3ObjectMeta - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientGetObject() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Get - suspend fun get(key: String): S3Object - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientGetManyMetas() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Get - suspend fun get(keys: Collection): List - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientGetManyObjects() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Get - suspend fun get(key: List): List - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientGetKeyConcat() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Get("{key1}-{key2}") - suspend fun get(key1: String, key2: Long): S3ObjectMeta - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientGetKeyMissing() { - val result = this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Get("{key1}-{key12345}") - suspend fun get(key1: String): S3ObjectMeta - } - """.trimIndent() - ) - assertThat(result.isFailed()).isTrue() - } - - @Test - fun clientGetKeyConst() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Get("const-key") - suspend fun get(): S3ObjectMeta - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientGetKeyUnused() { - val result = this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Get("const-key") - suspend fun get(key: String): S3ObjectMeta - } - """.trimIndent() - ) - assertThat(result.isFailed()).isTrue() - } - - // List - @Test - fun clientListMeta() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List - suspend fun list(): S3ObjectMetaList - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientListMetaWithPrefix() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List - suspend fun list(prefix: String): S3ObjectMetaList - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientListObjectsWithPrefix() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List - suspend fun list(prefix: String): S3ObjectList - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientListLimit() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List(limit = 100) - suspend fun list(prefix: String): S3ObjectList - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientListKeyConcat() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List("{key1}-{key2}") - suspend fun list(key1: String, key2: Long): S3ObjectList - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientListKeyAndDelimiter() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List(value = "some/path/to/{key1}/object", delimiter = "/") - suspend fun list(key1: String): S3ObjectList - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientListKeyMissing() { - val result = this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List("{key1}-{key12345}") - suspend fun list(key1: String): S3ObjectList - } - """.trimIndent() - ) - assertThat(result.isFailed()).isTrue() - } - - @Test - fun clientListKeyConst() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List("const-key") - suspend fun list(): S3ObjectList - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientListKeyUnused() { - val result = this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.List("const-key") - suspend fun list(key: String): S3ObjectList - } - """.trimIndent() - ) - assertThat(result.isFailed()).isTrue() - } - - // Delete - @Test - fun clientDelete() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Delete - suspend fun delete(key: String) - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientDeleteKeyConcat() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Delete("{key1}-{key2}") - suspend fun delete(key1: String, key2: Long) - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientDeleteKeyMissing() { - val result = this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Delete("{key1}-{key12345}") - suspend fun delete(key1: String) - } - """.trimIndent() - ) - assertThat(result.isFailed()).isTrue() - } - - @Test - fun clientDeleteKeyConst() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Delete("const-key") - suspend fun delete() - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientDeleteKeyUnused() { - val result = this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Delete("const-key") - suspend fun delete(key: String) - } - """.trimIndent() - ) - assertThat(result.isFailed()).isTrue() - } - - // Deletes - @Test - fun clientDeletes() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Delete - suspend fun delete(key: List) - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - // Put - @Test - fun clientPutBody() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Put - suspend fun put(key: String, value: S3Body) - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientPutBodyReturnUpload() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Put - suspend fun put(key: String, value: S3Body): S3ObjectUpload - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientPutBytes() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Put - suspend fun put(key: String, value: ByteArray) - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientPutBuffer() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Put - suspend fun put(key: String, value: ByteBuffer) - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientPutBodyAndType() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Put(type = "type") - suspend fun put(key: String, value: S3Body) - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientPutBodyAndEncoding() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Put(encoding = "encoding") - suspend fun put(key: String, value: S3Body) - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientPutBodyAndTypeAndEncoding() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Put(type = "type", encoding = "encoding") - suspend fun put(key: String, value: S3Body) - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientPutKeyConcat() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Put("{key1}-{key2}") - suspend fun put(key1: String, key2: Long, value: S3Body) - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientPutKeyMissing() { - val result = this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Put("{key1}-{key12345}") - suspend fun put(key1: String, value: S3Body) - } - """.trimIndent() - ) - assertThat(result.isFailed()).isTrue() - } - - @Test - fun clientPutKeyConst() { - this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Put("const-key") - suspend fun put(value: S3Body) - } - """.trimIndent() - ) - compileResult.assertSuccess() - val clazz = compileResult.loadClass("\$Client_Impl") - assertThat(clazz).isNotNull() - } - - @Test - fun clientPutKeyUnused() { - val result = this.compile0( - """ - @S3.Client("my") - interface Client { - - @S3.Put("const-key") - suspend fun put(key: String, value: S3Body) - } - """.trimIndent() - ) - assertThat(result.isFailed()).isTrue() - } -} diff --git a/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ListTest.kt b/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ListTest.kt new file mode 100644 index 000000000..43cac4abc --- /dev/null +++ b/experimental/s3-client-symbol-processor/src/test/kotlin/ru/tinkoff/kora/s3/client/symbol/processor/S3ListTest.kt @@ -0,0 +1,114 @@ +package ru.tinkoff.kora.s3.client.symbol.processor + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import ru.tinkoff.kora.s3.client.model.S3ObjectMeta + +class S3ListTest : AbstractS3Test() { + @Test + fun testListBucket() { + val client = this.compile(""" + @S3.Client + interface Client { + @S3.List + fun list(@S3.Bucket bucket: String): List + + @S3.List + fun iterator(@S3.Bucket bucket: String): Iterator + } + """.trimIndent()) + + val list = listOf() + whenever(s3Client.list("bucket", null, null, 1000)).thenReturn(list) + assertThat(client.invoke("list", "bucket")).isSameAs(list) + verify(s3Client).list("bucket", null, null, 1000) + reset(s3Client) + + val iterator = arrayListOf().iterator() + whenever(s3Client.listIterator("bucket", null, null, 1000)).thenReturn(iterator) + assertThat(client.invoke("iterator", "bucket")).isSameAs(iterator) + verify(s3Client).listIterator("bucket", null, null, 1000) + reset(s3Client) + } + + @Test + fun testListBucketPrefix() { + val client = this.compile(""" + @S3.Client + interface Client { + @S3.List + fun prefix(@S3.Bucket bucket: String, prefix: String): List + + @S3.List("/prefix/{prefix}") + fun template(@S3.Bucket bucket: String, prefix: String): List + } + """.trimIndent()) + + val list = listOf() + whenever(s3Client.list("bucket", "test1", null, 1000)).thenReturn(list) + assertThat(client.invoke("prefix", "bucket", "test1")).isSameAs(list) + verify(s3Client).list("bucket", "test1", null, 1000) + reset(s3Client) + + whenever(s3Client.list("bucket", "/prefix/test1", null, 1000)).thenReturn(list) + assertThat(client.invoke("template", "bucket", "test1")).isSameAs(list) + verify(s3Client).list("bucket", "/prefix/test1", null, 1000) + reset(s3Client) + } + + @Test + fun testLimit() { + val client = this.compile(""" + @S3.Client + interface Client { + @S3.List + fun onParameter(@S3.Bucket bucket: String, @S3.List.Limit limit: Int): List + + @S3.List + @S3.List.Limit(43) + fun onMethod(@S3.Bucket bucket: String): List + } + """.trimIndent()) + + val list = listOf() + whenever(s3Client.list("bucket", null, null, 42)).thenReturn(list) + assertThat(client.invoke("onParameter", "bucket", 42)).isSameAs(list) + verify(s3Client).list("bucket", null, null, 42) + reset(s3Client) + + whenever(s3Client.list("bucket", null, null, 43)).thenReturn(list) + assertThat(client.invoke("onMethod", "bucket")).isSameAs(list) + verify(s3Client).list("bucket", null, null, 43) + reset(s3Client) + } + + @Test + fun testListDelimiter() { + val client = this.compile(""" + @S3.Client + interface Client { + @S3.List + fun onParameter(@S3.Bucket bucket: String, @S3.List.Delimiter delimiter: String): List + + @S3.List + @S3.List.Delimiter("/") + fun onMethod(@S3.Bucket bucket: String): List + } + """.trimIndent()) + + val list = listOf() + whenever(s3Client.list("bucket", null, "test", 1000)).thenReturn(list) + assertThat(client.invoke("onParameter", "bucket", "test")).isSameAs(list) + verify(s3Client).list("bucket", null, "test", 1000) + reset(s3Client) + + whenever(s3Client.list("bucket", null, "/", 1000)).thenReturn(list) + assertThat(client.invoke("onMethod", "bucket")).isSameAs(list) + verify(s3Client).list("bucket", null, "/", 1000) + reset(s3Client) + } + +} diff --git a/experimental/s3-client-common/build.gradle b/experimental/s3-client/build.gradle similarity index 50% rename from experimental/s3-client-common/build.gradle rename to experimental/s3-client/build.gradle index d3557e9bb..a330e54e5 100644 --- a/experimental/s3-client-common/build.gradle +++ b/experimental/s3-client/build.gradle @@ -3,11 +3,16 @@ dependencies { compileOnly libs.jetbrains.annotations - implementation project(":config:config-common") implementation project(':logging:logging-common') api project(':telemetry:telemetry-common') + api project(":http:http-client-common") + api project(":config:config-common") api project(":common") testImplementation project(":internal:test-logging") + testImplementation project(":http:http-client-ok") + testImplementation libs.testcontainers.core + testImplementation libs.s3client.minio + testImplementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14' } diff --git a/experimental/s3-client/src/main/java/module-info.java b/experimental/s3-client/src/main/java/module-info.java new file mode 100644 index 000000000..298150c48 --- /dev/null +++ b/experimental/s3-client/src/main/java/module-info.java @@ -0,0 +1,12 @@ +module kora.s3_.client { + requires transitive kora.config.common; + requires transitive kora.http.client.common; + requires transitive org.jetbrains.annotations; + requires java.xml; + + exports ru.tinkoff.kora.s3.client; + exports ru.tinkoff.kora.s3.client.annotation; + exports ru.tinkoff.kora.s3.client.exception; + exports ru.tinkoff.kora.s3.client.model; + exports ru.tinkoff.kora.s3.client.telemetry; +} diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3Client.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3Client.java new file mode 100644 index 000000000..11b930723 --- /dev/null +++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3Client.java @@ -0,0 +1,59 @@ +package ru.tinkoff.kora.s3.client; + +import jakarta.annotation.Nullable; +import org.jetbrains.annotations.ApiStatus; +import ru.tinkoff.kora.s3.client.exception.S3ClientException; +import ru.tinkoff.kora.s3.client.model.S3Body; +import ru.tinkoff.kora.s3.client.model.S3Object; +import ru.tinkoff.kora.s3.client.model.S3ObjectMeta; +import ru.tinkoff.kora.s3.client.model.S3ObjectUploadResult; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +@ApiStatus.Experimental +public interface S3Client { + sealed interface RangeData { + /** + * @param from An integer indicating the start position of the request range, inclusive + * @param to An integer indicating the end position of the requested range, exclusive + */ + record Range(long from, long to) implements RangeData {} + + /** + * @param from An integer indicating the start position of the request range, inclusive + */ + record StartFrom(long from) implements RangeData {} + + /** + * @param bytes An integer indicating the number of bytes at the end of the resource, can be larger than resource size + */ + record LastN(long bytes) implements RangeData {} + } + + S3Object get(String bucket, String key, @Nullable RangeData range) throws S3ClientException; + + @Nullable + S3Object getOptional(String bucket, String key, @Nullable RangeData range) throws S3ClientException; + + S3ObjectMeta getMeta(String bucket, String key) throws S3ClientException; + + @Nullable + S3ObjectMeta getMetaOptional(String bucket, String key) throws S3ClientException; + + List list(String bucket, @Nullable String prefix, @Nullable String delimiter, int limit) throws S3ClientException; + + /** + * paginated listing by maxPageSize + */ + Iterator listIterator(String bucket, @Nullable String prefix, @Nullable String delimiter, int maxPageSize) throws S3ClientException; + + S3ObjectUploadResult put(String bucket, String key, S3Body body) throws S3ClientException; + + // todo version id ? + void delete(String bucket, String key) throws S3ClientException; + + void delete(String bucket, Collection keys) throws S3ClientException; + +} diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3ClientFactory.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3ClientFactory.java new file mode 100644 index 000000000..15a520572 --- /dev/null +++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3ClientFactory.java @@ -0,0 +1,5 @@ +package ru.tinkoff.kora.s3.client; + +public interface S3ClientFactory { + S3Client create(Class declarativeClientInterface); +} diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3ClientModule.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3ClientModule.java new file mode 100644 index 000000000..1161cf1db --- /dev/null +++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3ClientModule.java @@ -0,0 +1,37 @@ +package ru.tinkoff.kora.s3.client; + +import jakarta.annotation.Nullable; +import org.jetbrains.annotations.ApiStatus; +import ru.tinkoff.kora.common.DefaultComponent; +import ru.tinkoff.kora.config.common.Config; +import ru.tinkoff.kora.config.common.extractor.ConfigValueExtractor; +import ru.tinkoff.kora.http.client.common.HttpClient; +import ru.tinkoff.kora.http.client.common.telemetry.HttpClientTelemetryFactory; +import ru.tinkoff.kora.s3.client.impl.S3ClientImpl; +import ru.tinkoff.kora.s3.client.telemetry.*; + +@ApiStatus.Experimental +public interface S3ClientModule { + @DefaultComponent + default S3ClientFactory s3ClientFactory(HttpClient httpClient, S3Config config, S3TelemetryFactory telemetryFactory, HttpClientTelemetryFactory httpClientTelemetryFactory) { + return (clazz) -> new S3ClientImpl(httpClient, config, telemetryFactory, httpClientTelemetryFactory, clazz); + } + + default S3Config s3Config(Config config, ConfigValueExtractor extractor) { + var value = config.get("s3"); + return extractor.extract(value); + } + + @DefaultComponent + default S3LoggerFactory s3ClientLoggerFactory() { + return new DefaultS3LoggerFactory(); + } + + @DefaultComponent + default S3TelemetryFactory s3ClientTelemetryFactory(@Nullable S3LoggerFactory loggerFactory, + @Nullable S3TracerFactory tracingFactory, + @Nullable S3MetricsFactory metricsFactory) { + return new DefaultS3TelemetryFactory(loggerFactory, tracingFactory, metricsFactory); + } + +} diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3Config.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3Config.java new file mode 100644 index 000000000..fb0fb8504 --- /dev/null +++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/S3Config.java @@ -0,0 +1,65 @@ +package ru.tinkoff.kora.s3.client; + +import org.jetbrains.annotations.ApiStatus; +import ru.tinkoff.kora.common.util.Size; +import ru.tinkoff.kora.config.common.annotation.ConfigValueExtractor; +import ru.tinkoff.kora.telemetry.common.TelemetryConfig; + +import java.time.Duration; + +@ApiStatus.Experimental +@ConfigValueExtractor +public interface S3Config { + + String endpoint(); + + String accessKey(); + + String secretKey(); + + default String region() { + return "aws-global"; + } + + enum AddressStyle { + PATH, + VIRTUAL_HOSTED + } + + default AddressStyle addressStyle() { + return AddressStyle.PATH; + } + + default Duration requestTimeout() { + return Duration.ofSeconds(45); + } + + UploadConfig upload(); + + @ConfigValueExtractor + interface UploadConfig { + + /** + * 5 MiB to 5 GiB. There is no minimum size limit on the last part of your multipart upload. + */ + default Size partSize() { + return Size.of(5, Size.Type.MiB); + } + + /** + * The chunk size must be at least 8 KB. We recommend a chunk size of at least 64 KB for better performance. + */ + default Size chunkSize() { + return Size.of(64, Size.Type.KiB); + } + + /** + * In general, when your object size reaches 100 MB, you should consider using multipart uploads instead of uploading the object in a single operation. + */ + default Size singlePartUploadLimit() { + return Size.of(100, Size.Type.MiB); + } + } + + TelemetryConfig telemetry(); +} diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/annotation/S3.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/annotation/S3.java similarity index 58% rename from experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/annotation/S3.java rename to experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/annotation/S3.java index 1f5a88268..c67c4af94 100644 --- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/annotation/S3.java +++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/annotation/S3.java @@ -1,8 +1,6 @@ package ru.tinkoff.kora.s3.client.annotation; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Range; -import ru.tinkoff.kora.s3.client.S3ClientConfig; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -22,13 +20,37 @@ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.CLASS) @interface Client { + Class[] clientFactoryTag() default {}; + } + + @ApiStatus.Experimental + @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) + @Retention(RetentionPolicy.CLASS) + @interface Bucket { /** - * @return path for {@link S3ClientConfig} configuration + * @return path for bucket name in configuration *
{@code
-         *     @S3.Client("my-client")
+         *     @S3.Client
+         *     @Bucket("myClient.defaultBucket")
          *     interface SomeClient {
-         *
+         *         @S3.Get
+         *         byte[] get1(String key);
+         *     }
+         * }
+ *
{@code
+         *     @S3.Client
+         *     interface SomeClient {
+         *         @S3.Get
+         *         byte[] get1(@S3.Bucket String bucket, String key);
+         *     }
+         * }
+ *
{@code
+         *     @S3.Client
+         *     interface SomeClient {
+         *         @S3.Get
+         *         @S3.Bucket("myClient.get1Bucket)
+         *         byte[] get1(String key);
          *     }
          * }
*/ @@ -44,14 +66,18 @@ /** * @return Specifies key template or key constant: *
{@code
-         *     @S3.Client("my-client")
+         *     @S3.Client
+         *     @Bucket("bucket")
          *     interface SomeClient {
          *
          *         @S3.Get("prefix-{key}")
          *         S3Object get1(String key);
          *
          *         @S3.Get("const-key")
-         *         S3Object get2();
+         *         S3ObjectMeta get2();
+
+         *         @S3.Get("const-key")
+         *         byte[] get3();
          *     }
          * }
*/ @@ -66,34 +92,49 @@ /** * @return Specifies key template or key constant: *
{@code
-         *     @S3.Client("my-client")
+         *     @S3.Client
+         *     @Bucket("bucket")
          *     interface SomeClient {
          *
          *         @S3.List("pre-{prefix}")
-         *         S3ObjectList list1(String prefix);
+         *         List list1(String prefix);
          *
          *         @S3.List("const-key")
-         *         S3ObjectList list2();
+         *         Iterator list2();
          *     }
          * }
*/ String value() default ""; - /** - * @return Specifies key delimiter to exclude objects - *
{@code
-         *     @S3.Client("my-client")
-         *     interface SomeClient {
-         *
-         *         @S3.List(value = "some/object/path", delimiter = "/")
-         *         S3ObjectList list();
-         *     }
-         * }
- */ - String delimiter() default ""; - - @Range(from = 1, to = 1000) - int limit() default 1000; + @ApiStatus.Experimental + @Target({ElementType.PARAMETER, ElementType.METHOD}) + @Retention(RetentionPolicy.CLASS) + @interface Delimiter { + /** + * @return Specifies delimiter for list operation: + *
{@code
+             *     @S3.Client
+             *     @Bucket("bucket")
+             *     interface SomeClient {
+             *
+             *         @S3.List("pre-{prefix}")
+             *         List list1(String prefix, @S3.List.Delimiter String delimiter);
+             *
+             *         @S3.List("const-key")
+             *         @S3.List.Delimiter("/")
+             *         Iterator list2();
+             *     }
+             * }
+ */ + String value() default ""; + } + + @ApiStatus.Experimental + @Target({ElementType.PARAMETER, ElementType.METHOD}) + @Retention(RetentionPolicy.CLASS) + @interface Limit { + int value() default 1000; + } } @ApiStatus.Experimental @@ -104,7 +145,7 @@ /** * @return Specifies key template or key constant: *
{@code
-         *     @S3.Client("my-client")
+         *     @S3.Client
          *     interface SomeClient {
          *
          *         @S3.Put("prefix-{key}")
@@ -136,7 +177,7 @@
         /**
          * @return Specifies key template or key constant:
          * 
{@code
-         *     @S3.Client("my-client")
+         *     @S3.Client
          *     interface SomeClient {
          *
          *         @S3.Put("prefix-{key}")
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientErrorException.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientErrorException.java
new file mode 100644
index 000000000..e54d161da
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientErrorException.java
@@ -0,0 +1,34 @@
+package ru.tinkoff.kora.s3.client.exception;
+
+import jakarta.annotation.Nullable;
+
+public class S3ClientErrorException extends S3ClientResponseException {
+    private final String errorCode;
+    private final String errorMessage;
+    @Nullable
+    private final String requestId;
+
+    public S3ClientErrorException(int httpCode, String errorCode, String errorMessage, @Nullable String requestId) {
+        this(null, httpCode, errorCode, errorMessage, requestId);
+    }
+
+    public S3ClientErrorException(Throwable cause, int httpCode, String errorCode, String errorMessage, String requestId) {
+        super("S3ClientError: httpCode=%d, requestId=%s, errorCode=%s, errorMessage=%s".formatted(httpCode, requestId, errorMessage, errorMessage), cause, httpCode);
+        this.errorCode = errorCode;
+        this.errorMessage = errorMessage;
+        this.requestId = requestId;
+    }
+
+    public String getErrorCode() {
+        return errorCode;
+    }
+
+    public String getErrorMessage() {
+        return errorMessage;
+    }
+
+    @Nullable
+    public String getRequestId() {
+        return requestId;
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientException.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientException.java
new file mode 100644
index 000000000..3480b7cba
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientException.java
@@ -0,0 +1,18 @@
+package ru.tinkoff.kora.s3.client.exception;
+
+public abstract class S3ClientException extends RuntimeException {
+    public S3ClientException() {
+    }
+
+    public S3ClientException(String message) {
+        super(message);
+    }
+
+    public S3ClientException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public S3ClientException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientResponseException.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientResponseException.java
new file mode 100644
index 000000000..ef389f28a
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientResponseException.java
@@ -0,0 +1,24 @@
+package ru.tinkoff.kora.s3.client.exception;
+
+public class S3ClientResponseException extends S3ClientException {
+    private final int httpCode;
+
+    public S3ClientResponseException(int httpCode) {
+        this.httpCode = httpCode;
+    }
+
+    public S3ClientResponseException(String message, int httpCode) {
+        super(message);
+        this.httpCode = httpCode;
+    }
+
+    public S3ClientResponseException(String message, Throwable cause, int httpCode) {
+        super(message, cause);
+        this.httpCode = httpCode;
+    }
+
+    public S3ClientResponseException(Throwable cause, int httpCode) {
+        super(cause);
+        this.httpCode = httpCode;
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientUnknownException.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientUnknownException.java
new file mode 100644
index 000000000..bfd6dbc6d
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientUnknownException.java
@@ -0,0 +1,18 @@
+package ru.tinkoff.kora.s3.client.exception;
+
+public final class S3ClientUnknownException extends S3ClientException {
+    public S3ClientUnknownException() {
+    }
+
+    public S3ClientUnknownException(String message) {
+        super(message);
+    }
+
+    public S3ClientUnknownException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public S3ClientUnknownException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/ByteArrayS3Body.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/ByteArrayS3Body.java
new file mode 100644
index 000000000..3bc957ff0
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/ByteArrayS3Body.java
@@ -0,0 +1,60 @@
+package ru.tinkoff.kora.s3.client.impl;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+import ru.tinkoff.kora.s3.client.model.S3Body;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Objects;
+
+@ApiStatus.Experimental
+public record ByteArrayS3Body(
+    byte[] bytes,
+    int offset,
+    int len,
+    @Nullable String contentType,
+    @Nullable String encoding) implements S3Body {
+
+    @Override
+    public byte[] asBytes() {
+        if (offset == 0 && len == bytes.length) {
+            return bytes;
+        }
+        return Arrays.copyOfRange(bytes, offset, offset + len);
+    }
+
+    @Override
+    public InputStream asInputStream() {
+        return new ByteArrayInputStream(bytes, offset, len);
+    }
+
+    @Override
+    public long size() {
+        return this.len;
+    }
+
+    @Override
+    public void close() throws IOException {
+
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof ByteArrayS3Body that) {
+            return len == that.len
+                && Objects.equals(encoding, that.encoding)
+                && Objects.equals(contentType, that.contentType)
+                && Arrays.equals(bytes, offset, offset + len, that.bytes, that.offset, that.offset + len)
+                ;
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(len, contentType, encoding);
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/DigestUtils.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/DigestUtils.java
new file mode 100644
index 000000000..eb5248a5a
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/DigestUtils.java
@@ -0,0 +1,77 @@
+package ru.tinkoff.kora.s3.client.impl;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.HexFormat;
+
+public class DigestUtils {
+
+    public static MessageDigest md5() {
+        try {
+            return MessageDigest.getInstance("MD5");
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    public static MessageDigest sha256() {
+        try {
+            return MessageDigest.getInstance("SHA-256");
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    public record Digest(byte[] digest) {
+        public String hex() {
+            return HexFormat.of().formatHex(digest);
+        }
+
+        public String base64() {
+            return Base64.getEncoder().encodeToString(digest);
+        }
+    }
+
+    public static Digest sha256(byte[] data, int offset, int length) {
+        var digest = sha256();
+        digest.update(data, offset, length);
+        return new Digest(digest.digest());
+    }
+
+    public static Digest md5(byte[] data, int off, int len) {
+        var md5 = md5();
+        md5.update(data, off, len);
+        return new Digest(md5.digest());
+    }
+
+    public static Digest sha256(String str) {
+        var bytes = str.getBytes(StandardCharsets.UTF_8);
+        return sha256(bytes, 0, bytes.length);
+    }
+
+    public static byte[] sumHmac(byte[] key, byte[] data) {
+        try {
+            var mac = Mac.getInstance("HmacSHA256");
+            mac.init(new SecretKeySpec(key, "HmacSHA256"));
+            mac.update(data);
+            return mac.doFinal();
+        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+            throw new IllegalStateException(e); // never gonna happen
+        }
+    }
+
+    public static byte[] sumHmac(Mac key, byte[] data) {
+        try {
+            var mac = (Mac) key.clone();
+            mac.update(data);
+            return mac.doFinal();
+        } catch (CloneNotSupportedException e) {
+            throw new IllegalStateException(e); // never gonna happen
+        }
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/InputStreamS3Body.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/InputStreamS3Body.java
new file mode 100644
index 000000000..fbb05fb1c
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/InputStreamS3Body.java
@@ -0,0 +1,66 @@
+package ru.tinkoff.kora.s3.client.impl;
+
+import jakarta.annotation.Nullable;
+import org.jetbrains.annotations.ApiStatus;
+import ru.tinkoff.kora.s3.client.model.S3Body;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+@ApiStatus.Experimental
+public final class InputStreamS3Body implements S3Body {
+    private final InputStream inputStream;
+    private final long size;
+    @Nullable
+    private final String contentType;
+    @Nullable
+    private final String encoding;
+
+    public InputStreamS3Body(InputStream inputStream, long size, @Nullable String contentType, @Nullable String encoding) {
+        this.inputStream = Objects.requireNonNull(inputStream);
+        this.size = size;
+        this.contentType = contentType;
+        this.encoding = encoding;
+    }
+
+    @Override
+    public InputStream asInputStream() {
+        return inputStream;
+    }
+
+    @Override
+    public void close() throws IOException {
+        this.inputStream.close();
+    }
+
+    public InputStream getInputStream() {
+        return inputStream;
+    }
+
+    @Override
+    public long size() {
+        return size;
+    }
+
+    @Override
+    @Nullable
+    public String contentType() {
+        return contentType;
+    }
+
+    @Override
+    @Nullable
+    public String encoding() {
+        return encoding;
+    }
+
+    @Override
+    public String toString() {
+        return "InputStreamS3Body[" +
+            "size=" + size + ", " +
+            "contentType=" + contentType + ", " +
+            "encoding=" + encoding + ']';
+    }
+
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/KnownSizeAwsChunkedHttpBody.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/KnownSizeAwsChunkedHttpBody.java
new file mode 100644
index 000000000..36ecab044
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/KnownSizeAwsChunkedHttpBody.java
@@ -0,0 +1,113 @@
+package ru.tinkoff.kora.s3.client.impl;
+
+import jakarta.annotation.Nullable;
+import ru.tinkoff.kora.http.common.body.HttpBodyOutput;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.Base64;
+import java.util.concurrent.Flow;
+
+class KnownSizeAwsChunkedHttpBody implements HttpBodyOutput {
+    // ;chunk-signature=b6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9\r\n\r\n
+    private static final int AWS_CHUNK_SUFFIX_SIZE = 85;
+    // 0;chunk-signature=b6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9\r\n\r\n
+    private static final int AWS_CHUNK_ZERO_HEADER_SIZE = AWS_CHUNK_SUFFIX_SIZE + 1;
+    // x-amz-checksum-sha256:YeJXRE0UIVNLcFUlWAzyOcOh/42uhL5Q1Az8JdFNNus=\r\n
+    // x-amz-trailer-signature:63bddb248ad2590c92712055f51b8e78ab024eead08276b24f010b0efd74843f
+    private static final int SHA256_TRAILER_SIZE = 159;
+
+    private final S3RequestSigner signer;
+    private final String contentType;
+    private final InputStream is;
+    private final MessageDigest sha256 = DigestUtils.sha256();
+    private final long contentLength;
+    private final byte[] buf;
+    final ZonedDateTime date;
+    @Nullable
+    private String shaBase64;
+    String previousSignature;
+
+    static long calculateFullBodyLength(long uploadChunkSize, String uploadChunkSizeHex, long size, @Nullable String sha256Base64) {
+        var lastDataChunkSize = size % uploadChunkSize;
+        var fullChunks = (long) Math.floor((double) size / uploadChunkSize);
+        var fullChunkSize = uploadChunkSizeHex.length() + AWS_CHUNK_SUFFIX_SIZE + uploadChunkSize;
+        var lastChunkSize = lastDataChunkSize == 0 ? 0 : lastDataChunkSize + Long.toHexString(lastDataChunkSize).length() + AWS_CHUNK_SUFFIX_SIZE;
+        var trailer = sha256Base64 == null ? SHA256_TRAILER_SIZE : 0;
+
+        return fullChunks * fullChunkSize
+            + lastChunkSize
+            + trailer
+            + AWS_CHUNK_ZERO_HEADER_SIZE;
+    }
+
+    KnownSizeAwsChunkedHttpBody(S3RequestSigner signer, int uploadChunkSize, String uploadChunkSizeHex, String contentType, InputStream is, long streamLength, @Nullable String sha256Base64) {
+        this.signer = signer;
+        this.date = ZonedDateTime.now(ZoneOffset.UTC);
+        this.contentType = contentType;
+        this.is = is;
+        this.buf = new byte[uploadChunkSize];
+        this.contentLength = calculateFullBodyLength(uploadChunkSize, uploadChunkSizeHex, streamLength, sha256Base64);
+        this.shaBase64 = sha256Base64;
+    }
+
+    @Override
+    public long contentLength() {
+        return contentLength;
+    }
+
+    @Override
+    @Nullable
+    public String contentType() {
+        return this.contentType;
+    }
+
+    @Override
+    public void subscribe(Flow.Subscriber subscriber) {
+        throw new IllegalStateException();
+    }
+
+    @Override
+    public void close() throws IOException {
+        is.close();
+    }
+
+    @Override
+    public void write(OutputStream os) throws IOException {
+        while (true) {
+            var read = is.readNBytes(buf, 0, buf.length);
+            if (read <= 0) {
+                previousSignature = signer.processFinalChunk(date, previousSignature, os);
+                break;
+            }
+            if (this.shaBase64 == null) {
+                sha256.update(buf, 0, read);
+            }
+            previousSignature = signer.processChunk(date, previousSignature, buf, read, os);
+            os.flush();
+            if (read < buf.length) {
+                previousSignature = signer.processFinalChunk(date, previousSignature, os);
+                break;
+            }
+        }
+        if (shaBase64 == null) {
+            this.shaBase64 = Base64.getEncoder().encodeToString(sha256.digest());
+            signer.processTrailer(date, previousSignature, shaBase64, os);
+        } else {
+            os.write("\r\n".getBytes(StandardCharsets.US_ASCII));
+        }
+    }
+
+    public String sha256() {
+        if (this.shaBase64 != null) {
+            return this.shaBase64;
+        }
+        throw new IllegalStateException("Can't get sha256 hash before body has been written");
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3ClientImpl.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3ClientImpl.java
new file mode 100644
index 000000000..0e8cbe383
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3ClientImpl.java
@@ -0,0 +1,396 @@
+package ru.tinkoff.kora.s3.client.impl;
+
+import jakarta.annotation.Nullable;
+import ru.tinkoff.kora.http.client.common.HttpClient;
+import ru.tinkoff.kora.http.client.common.interceptor.TelemetryInterceptor;
+import ru.tinkoff.kora.http.client.common.request.HttpClientRequest;
+import ru.tinkoff.kora.http.client.common.response.HttpClientResponse;
+import ru.tinkoff.kora.http.client.common.telemetry.HttpClientTelemetryFactory;
+import ru.tinkoff.kora.http.common.body.HttpBody;
+import ru.tinkoff.kora.http.common.body.HttpBodyInput;
+import ru.tinkoff.kora.http.common.header.HttpHeaders;
+import ru.tinkoff.kora.s3.client.S3Client;
+import ru.tinkoff.kora.s3.client.S3Config;
+import ru.tinkoff.kora.s3.client.exception.S3ClientErrorException;
+import ru.tinkoff.kora.s3.client.exception.S3ClientException;
+import ru.tinkoff.kora.s3.client.exception.S3ClientResponseException;
+import ru.tinkoff.kora.s3.client.exception.S3ClientUnknownException;
+import ru.tinkoff.kora.s3.client.impl.xml.DeleteObjectsRequest;
+import ru.tinkoff.kora.s3.client.impl.xml.DeleteObjectsResult;
+import ru.tinkoff.kora.s3.client.impl.xml.ListBucketResult;
+import ru.tinkoff.kora.s3.client.impl.xml.S3Error;
+import ru.tinkoff.kora.s3.client.model.S3Body;
+import ru.tinkoff.kora.s3.client.model.S3Object;
+import ru.tinkoff.kora.s3.client.model.S3ObjectMeta;
+import ru.tinkoff.kora.s3.client.model.S3ObjectUploadResult;
+import ru.tinkoff.kora.s3.client.telemetry.S3Telemetry;
+import ru.tinkoff.kora.s3.client.telemetry.S3TelemetryFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+public class S3ClientImpl implements S3Client {
+    private final HttpClient httpClient;
+    private final S3Config config;
+    private final S3Telemetry telemetry;
+    private final S3RequestSigner signer;
+    private final S3PutHelper putHelper;
+    private final UriHelper uriHelper;
+
+    public S3ClientImpl(HttpClient httpClient, S3Config config, S3TelemetryFactory telemetryFactory, HttpClientTelemetryFactory httpClientTelemetryFactory, Class declarativeClientInterface) {
+        this.httpClient = httpClient.with(new TelemetryInterceptor(httpClientTelemetryFactory.get(config.telemetry(), declarativeClientInterface.getSimpleName())));
+        this.config = config;
+        this.uriHelper = new UriHelper(config);
+        this.telemetry = telemetryFactory.get(config.telemetry(), declarativeClientInterface);
+        this.signer = new S3RequestSigner(config.accessKey(), config.secretKey(), config.region());
+        this.putHelper = new S3PutHelper(httpClient, config, telemetry, uriHelper, signer);
+    }
+
+    @Override
+    @Nullable
+    public S3Object get(String bucket, String key, @Nullable RangeData range) {
+        return this.getInternal(bucket, key, false, range);
+    }
+
+    @Override
+    @Nullable
+    public S3Object getOptional(String bucket, String key, @Nullable RangeData range) {
+        return this.getInternal(bucket, key, true, range);
+    }
+
+    @Nullable
+    public S3Object getInternal(String bucket, String key, boolean isOptional, RangeData range) {
+        try (var telemetry = this.telemetry.getObject(bucket, key);) {
+            try {
+                var headers = HttpHeaders.of();
+                if (range instanceof RangeData.Range r) {
+                    headers.add("range", "bytes=" + r.from() + "-" + r.to());
+                } else if (range instanceof RangeData.StartFrom r) {
+                    headers.add("range", "bytes=" + r.from() + "-");
+                } else if (range instanceof RangeData.LastN r) {
+                    headers.add("range", "bytes=-" + r.bytes());
+                } else if (range != null) {
+                    throw new IllegalStateException("not gonna happen");
+                }
+                var uri = this.uriHelper.uri(bucket, key);
+                var request = HttpClientRequest.of("GET", uri, "/{bucket}/{object}", headers, HttpBody.empty(), this.config.requestTimeout());
+                this.signer.processRequest(request, S3RequestSigner.EMPTY_QUERY, Map.of(), S3RequestSigner.EMPTY_PAYLOAD_SHA256);
+                var rs = this.httpClient.execute(request).toCompletableFuture().get();
+                telemetry.setAwsRequestId(rs.headers().getFirst("X-Amz-Request-Id"));
+                telemetry.setAwsExtendedId(rs.headers().getFirst("x-amz-id-2"));
+                if (rs.code() == 200) {
+                    var lastModified = rs.headers().getFirst("Last-Modified");
+                    var modified = lastModified != null
+                        ? Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(lastModified))
+                        : null;
+                    var body = rs.body();
+                    var s3Body = new InputStreamS3Body(rs.body().asInputStream(), body.contentLength(), body.contentType(), rs.headers().getFirst("Content-Encoding"));
+                    var s3Meta = new S3ObjectMeta(bucket, key, modified, body.contentLength());
+                    return new S3Object(s3Meta, s3Body);
+                }
+                if (rs.code() == 206) {
+                    var lastModified = rs.headers().getFirst("Last-Modified");
+                    var modified = lastModified != null
+                        ? Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(lastModified))
+                        : null;
+                    var body = rs.body();
+                    var s3Body = new InputStreamS3Body(rs.body().asInputStream(), body.contentLength(), body.contentType(), rs.headers().getFirst("Content-Encoding"));
+                    var size = s3Body.size();
+                    var contentRange = rs.headers().getFirst("Content-Range");
+                    if (contentRange != null) {
+                        if (contentRange.startsWith("bytes ")) {
+                            var i = contentRange.indexOf('/');
+                            if (i >= 0) {
+                                try {
+                                    size = Long.parseLong(contentRange.substring(i + 1));
+                                } catch (NumberFormatException ignore) {}
+                            }
+                        }
+                    }
+                    var s3Meta = new S3ObjectMeta(bucket, key, modified, size);
+                    return new S3Object(s3Meta, s3Body);
+                }
+                if (rs.code() == 404 && isOptional) {
+                    rs.close();
+                    return null;
+                }
+                try (var ignore = rs; var body = rs.body()) {
+                    throw parseS3Exception(rs, body);
+                }
+            } catch (S3ClientException e) {
+                telemetry.setError(e);
+                throw e;
+            } catch (Exception e) {
+                telemetry.setError(e);
+                throw new S3ClientUnknownException(e);
+            }
+        }
+    }
+
+    @Override
+    @Nullable
+    public S3ObjectMeta getMeta(String bucket, String key) {
+        return this.getMetaInternal(bucket, key, false);
+    }
+
+    @Override
+    @Nullable
+    public S3ObjectMeta getMetaOptional(String bucket, String key) {
+        return this.getMetaInternal(bucket, key, true);
+    }
+
+    @Nullable
+    public S3ObjectMeta getMetaInternal(String bucket, String key, boolean isOptional) {
+        try (var telemetry = this.telemetry.getMetadata(bucket, key);) {
+            var headers = HttpHeaders.of();
+            var uri = this.uriHelper.uri(bucket, key);
+            var request = HttpClientRequest.of("HEAD", uri, "/{bucket}/{object}", headers, HttpBody.empty(), this.config.requestTimeout());
+            this.signer.processRequest(request, S3RequestSigner.EMPTY_QUERY, Map.of(), S3RequestSigner.EMPTY_PAYLOAD_SHA256);
+            try (var rs = this.httpClient.execute(request).toCompletableFuture().get()) {
+                var amxRequestId = rs.headers().getFirst("X-Amz-Request-Id");
+                telemetry.setAwsRequestId(amxRequestId);
+                telemetry.setAwsExtendedId(rs.headers().getFirst("x-amz-id-2"));
+                if (rs.code() == 200) {
+                    var accessor = DateTimeFormatter.RFC_1123_DATE_TIME.parse(rs.headers().getFirst("Last-Modified"));
+                    var modified = Instant.from(accessor);
+                    var contentLength = rs.headers().getFirst("content-length");
+                    var contentLengthLong = contentLength == null
+                        ? 0L
+                        : Long.parseLong(contentLength);
+                    return new S3ObjectMeta(bucket, key, modified, contentLengthLong);
+                }
+                if (rs.code() == 404) {
+                    if (isOptional) {
+                        return null;
+                    } else {
+                        throw new S3ClientErrorException(rs.code(), "NoSuchKey", "Object does not exist", amxRequestId);
+                    }
+                }
+                try (var body = rs.body()) {
+                    throw parseS3Exception(rs, body);
+                }
+            } catch (S3ClientException e) {
+                telemetry.setError(e);
+                throw e;
+            } catch (Exception e) {
+                telemetry.setError(e);
+                throw new S3ClientUnknownException(e);
+            }
+        }
+    }
+
+    @Override
+    public List list(String bucket, @Nullable String prefix, @Nullable String delimiter, int limit) {
+        return this.listInternal(bucket, prefix, delimiter, limit, null).objects();
+    }
+
+    @Override
+    public Iterator listIterator(String bucket, @Nullable String prefix, @Nullable String delimiter, int maxPageSize) {
+        var first = this.listInternal(bucket, prefix, delimiter, maxPageSize, null);
+        if (first.continuationToken() == null) {
+            return first.objects().iterator();
+        }
+        return new Iterator<>() {
+            private ListResult currentList = first;
+            private Iterator currentIterator = first.objects().iterator();
+
+            @Override
+            public boolean hasNext() {
+                if (currentIterator.hasNext()) {
+                    return true;
+                }
+                if (currentList.continuationToken() == null) {
+                    return false;
+                }
+                currentList = listInternal(bucket, prefix, delimiter, maxPageSize, currentList.continuationToken());
+                currentIterator = currentList.objects().iterator();
+                return currentIterator.hasNext();
+            }
+
+            @Override
+            public S3ObjectMeta next() {
+                return currentIterator.next();
+            }
+        };
+    }
+
+
+    private record ListResult(List objects, @Nullable String continuationToken) {}
+
+    private ListResult listInternal(String bucket, @Nullable String prefix, @Nullable String delimiter, int limit, @Nullable String continuationToken) {
+        try (var telemetry = this.telemetry.listMetadata(bucket, prefix, delimiter)) {
+            var pathBuilder = new StringBuilder("?list-type=2");
+            var queryParameters = new TreeMap();
+            queryParameters.put("list-type", "2");
+            if (delimiter != null) {
+                var encoded = URLEncoder.encode(delimiter, StandardCharsets.UTF_8);
+                queryParameters.put("delimiter", encoded);
+                pathBuilder.append("&delimiter=").append(encoded);
+            }
+            if (prefix != null) {
+                var encoded = URLEncoder.encode(prefix, StandardCharsets.UTF_8);
+                queryParameters.put("prefix", encoded);
+                pathBuilder.append("&prefix=").append(encoded);
+            }
+            if (continuationToken != null) {
+                var encoded = URLEncoder.encode(continuationToken, StandardCharsets.UTF_8);
+                queryParameters.put("continuation-token", encoded);
+                pathBuilder.append("&continuation-token=").append(encoded);
+            }
+            queryParameters.put("max-keys", Integer.toString(limit));
+            pathBuilder.append("&max-keys=").append(limit);
+
+            var headers = HttpHeaders.of();
+            var uri = this.uriHelper.uri(bucket, pathBuilder.toString());
+            var request = HttpClientRequest.of("GET", uri, "/{bucket}?list", headers, HttpBody.empty(), this.config.requestTimeout());
+            this.signer.processRequest(request, queryParameters, Map.of(), S3RequestSigner.EMPTY_PAYLOAD_SHA256);
+            try (var rs = this.httpClient.execute(request).toCompletableFuture().get();
+                 var body = rs.body()) {
+                telemetry.setAwsRequestId(rs.headers().getFirst("X-Amz-Request-Id"));
+                telemetry.setAwsExtendedId(rs.headers().getFirst("x-amz-id-2"));
+                if (rs.code() == 200) {
+                    try (var is = body.asInputStream()) {
+                        var listResult = ListBucketResult.fromXml(is);
+                        var result = new ArrayList(listResult.contents().size());
+                        for (var content : listResult.contents()) {
+                            var accessor = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(content.lastModified());
+                            var modified = Instant.from(accessor);
+                            var meta = new S3ObjectMeta(bucket, content.key(), modified, content.size());
+                            result.add(meta);
+                        }
+                        return new ListResult(result, listResult.nextContinuationToken());
+                    }
+                }
+                throw parseS3Exception(rs, body);
+            } catch (S3ClientException e) {
+                telemetry.setError(e);
+                throw e;
+            } catch (Exception e) {
+                telemetry.setError(e);
+                throw new S3ClientUnknownException(e);
+            }
+        }
+    }
+
+    @Override
+    public void delete(String bucket, String key) {
+        try (var telemetry = this.telemetry.deleteObject(bucket, key);) {
+            var headers = HttpHeaders.of();
+            var uri = this.uriHelper.uri(bucket, key);
+            var request = HttpClientRequest.of("DELETE", uri, "/{bucket}/{key}", headers, HttpBody.empty(), this.config.requestTimeout());
+            this.signer.processRequest(request, S3RequestSigner.EMPTY_QUERY, Map.of(), S3RequestSigner.EMPTY_PAYLOAD_SHA256);
+            try (var rs = this.httpClient.execute(request).toCompletableFuture().get();
+                 var body = rs.body()) {
+                telemetry.setAwsRequestId(rs.headers().getFirst("X-Amz-Request-Id"));
+                telemetry.setAwsExtendedId(rs.headers().getFirst("x-amz-id-2"));
+                if (rs.code() == 204) {
+                    return;
+                }
+                if (rs.code() == 404) { // no such bucket
+                    return;
+                }
+                throw parseS3Exception(rs, body);
+            } catch (S3ClientException e) {
+                telemetry.setError(e);
+                throw e;
+            } catch (Exception e) {
+                telemetry.setError(e);
+                throw new S3ClientUnknownException(e);
+            }
+        }
+    }
+
+    @Override
+    public void delete(String bucket, Collection keys) {
+        try (var telemetry = this.telemetry.deleteObjects(bucket, keys);) {
+            var xml = DeleteObjectsRequest.toXml(keys.stream().map(DeleteObjectsRequest.S3Object::new)::iterator);
+            var payloadSha256 = DigestUtils.sha256(xml, 0, xml.length).hex();
+            var bodyMd5 = DigestUtils.md5(xml, 0, xml.length).base64();
+            var headers = Map.of(
+                "content-md5", bodyMd5
+            );
+            var uri = this.uriHelper.uri(bucket, "?delete=true");
+            var request = HttpClientRequest.of("POST", uri, "/{bucket}", HttpHeaders.of(), HttpBody.of(xml), this.config.requestTimeout());
+            this.signer.processRequest(request, new TreeMap<>(Map.of("delete", "true")), headers, payloadSha256);
+            try (var rs = this.httpClient.execute(request).toCompletableFuture().get();
+                 var body = rs.body()) {
+                telemetry.setAwsRequestId(rs.headers().getFirst("X-Amz-Request-Id"));
+                telemetry.setAwsExtendedId(rs.headers().getFirst("x-amz-id-2"));
+                if (rs.code() == 200) {
+                    var ignore = DeleteObjectsResult.fromXml(body.asInputStream());
+                    return;
+                }
+                if (rs.code() == 404) { // no such bucket
+                    return;
+                }
+                throw parseS3Exception(rs, body);
+            } catch (S3ClientException e) {
+                telemetry.setError(e);
+                throw e;
+            } catch (Exception e) {
+                telemetry.setError(e);
+                throw new S3ClientUnknownException(e);
+            }
+        }
+    }
+
+    @Override
+    public S3ObjectUploadResult put(String bucket, String key, S3Body body) {
+        try (var telemetry = this.telemetry.putObject(bucket, key, body.size())) {
+            try {
+                if (body instanceof ByteArrayS3Body bytes) {
+                    if (bytes.len() > this.putHelper.singlePartLimit * 2) {
+                        return this.putHelper.putByteArrayMultipart(bucket, key, body.encoding(), body.contentType(), bytes.bytes(), bytes.offset(), bytes.len());
+                    }
+                    return this.putHelper.putFullBody(bucket, key, body.encoding(), body.contentType(), bytes.bytes(), bytes.offset(), bytes.len());
+                }
+                if (body instanceof InputStreamS3Body stream) {
+                    try (var is = stream.getInputStream()) {
+                        if (body.size() > -1) {
+                            if (stream.size() <= this.putHelper.uploadPartSize) {
+                                // just download whole body and upload in one chunk
+                                var bytes = is.readNBytes((int) stream.size());
+                                return this.putHelper.putFullBody(bucket, key, stream.encoding(), stream.contentType(), bytes, 0, bytes.length);
+                            }
+                            if (stream.size() > this.putHelper.singlePartLimit * 2L) {
+                                // we should multipart upload it
+                                return this.putHelper.putKnownSizeBodyMultiparted(bucket, key, stream.contentType(), stream.encoding(), is, body.size());
+                            }
+                            // let's use aws-chunked encoding
+                            return this.putHelper.putKnownSizeBody(bucket, key, stream.contentType(), stream.encoding(), is, body.size());
+                        }
+                        return this.putHelper.putUnknownSizeBody(bucket, key, stream);
+                    } catch (IOException e) {
+                        throw new S3ClientUnknownException(e);
+                    }
+                }
+                throw new IllegalStateException("Unknown body type: " + body.getClass());
+            } catch (Throwable t) {
+                telemetry.setError(t);
+                throw t;
+            }
+        }
+    }
+
+
+    static RuntimeException parseS3Exception(HttpClientResponse rs, HttpBodyInput body) {
+        try (var is = body.asInputStream()) {
+            var bytes = is.readAllBytes();
+            try {
+                var s3Error = S3Error.fromXml(new ByteArrayInputStream(bytes));
+                throw new S3ClientErrorException(rs.code(), s3Error.code(), s3Error.message(), s3Error.requestId());
+            } catch (S3ClientException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new S3ClientResponseException("Unexpected response from s3: code=%s, body=%s".formatted(rs.code(), new String(bytes, StandardCharsets.UTF_8)), e, rs.code());
+            }
+        } catch (IOException e) {
+            throw new S3ClientUnknownException(e);
+        }
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3PutHelper.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3PutHelper.java
new file mode 100644
index 000000000..97835efdf
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3PutHelper.java
@@ -0,0 +1,412 @@
+package ru.tinkoff.kora.s3.client.impl;
+
+import jakarta.annotation.Nullable;
+import ru.tinkoff.kora.common.util.Size;
+import ru.tinkoff.kora.http.client.common.HttpClient;
+import ru.tinkoff.kora.http.client.common.request.HttpClientRequest;
+import ru.tinkoff.kora.http.common.body.HttpBody;
+import ru.tinkoff.kora.http.common.header.HttpHeaders;
+import ru.tinkoff.kora.s3.client.S3Config;
+import ru.tinkoff.kora.s3.client.exception.S3ClientException;
+import ru.tinkoff.kora.s3.client.exception.S3ClientUnknownException;
+import ru.tinkoff.kora.s3.client.impl.xml.CompleteMultipartUploadRequest;
+import ru.tinkoff.kora.s3.client.impl.xml.CompleteMultipartUploadResult;
+import ru.tinkoff.kora.s3.client.impl.xml.InitiateMultipartUploadResult;
+import ru.tinkoff.kora.s3.client.model.S3ObjectUploadResult;
+import ru.tinkoff.kora.s3.client.telemetry.S3Telemetry;
+
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.*;
+
+class S3PutHelper {
+    private final HttpClient httpClient;
+    private final S3RequestSigner signer;
+    final int uploadChunkSize;
+    final int uploadPartSize;
+    final String uploadChunkSizeHex;
+    final int singlePartLimit;
+    private final UriHelper uriHelper;
+    private final Duration requestTimeout;
+    private final S3Telemetry telemetry;
+
+    S3PutHelper(HttpClient httpClient, S3Config config, S3Telemetry telemetry, UriHelper uriHelper, S3RequestSigner signer) {
+        this.httpClient = httpClient;
+        this.signer = signer;
+        this.uriHelper = uriHelper;
+        this.requestTimeout = config.requestTimeout();
+        this.telemetry = telemetry;
+        var uploadChunkSize = Math.toIntExact(config.upload().chunkSize().toBytes());
+        if (uploadChunkSize < 0) {
+            throw new IllegalArgumentException("uploadChunkSize must be >= 0");
+        }
+        var uploadPartSize = Math.toIntExact(config.upload().partSize().toBytes());
+        if (uploadPartSize < 0) {
+            throw new IllegalArgumentException("uploadPartSize must be >= 0");
+        }
+        if (uploadPartSize < Size.of(5, Size.Type.MiB).toBytes()) {
+            throw new IllegalArgumentException("uploadPartSize must be >= 5mb");
+        }
+        this.singlePartLimit = Math.toIntExact(config.upload().singlePartUploadLimit().toBytes());
+        this.uploadChunkSize = uploadChunkSize;
+        this.uploadChunkSizeHex = Integer.toHexString(uploadChunkSize);
+        this.uploadPartSize = uploadPartSize;
+    }
+
+    S3ObjectUploadResult putByteArrayMultipart(String bucket, String key, String contentEncoding, String contentType, byte[] buf, int off, int len) {
+        var startMultipart = this.startMultipartUpload(bucket, key, contentEncoding, contentType);
+        var completeRq = new CompleteMultipartUploadRequest(new ArrayList<>());
+        try {
+            var queryParams = new TreeMap();
+            queryParams.put("uploadId", startMultipart.uploadId());
+            for (int i = 0; i < Integer.MAX_VALUE; i++) {
+                var from = i * singlePartLimit;
+                if (from >= len) {
+                    break;
+                }
+                var partNumber = String.valueOf(i + 1);
+                var to = Math.min(len, from + singlePartLimit);
+                var partLen = to - from;
+                queryParams.put("partNumber", partNumber);
+                var uri = this.uriHelper.uri(bucket, key + "?partNumber=" + partNumber + "&uploadId=" + startMultipart.uploadId());
+                var sha256 = DigestUtils.sha256(buf, off + from, partLen).base64();
+                try (var telemetry = this.telemetry.putObjectPart(bucket, key, startMultipart.uploadId(), i + 1, partLen)) {
+                    telemetry.setUploadId(startMultipart.uploadId());
+                    try {
+                        var part = this.putAwsChunked(telemetry, uri, queryParams, partLen, null, null, new ByteArrayInputStream(buf, off + from, partLen), sha256);
+                        completeRq.parts().add(new CompleteMultipartUploadRequest.Part(part.etag(), i + 1, part.sha256()));
+                    } catch (Throwable t) {
+                        telemetry.setError(t);
+                        throw t;
+                    }
+                }
+            }
+        } catch (Throwable t) {
+            try {
+                this.abortUpload(bucket, key, startMultipart.uploadId());
+            } catch (Throwable abortError) {
+                t.addSuppressed(abortError);
+            }
+            throw t;
+        }
+        return this.completeUpload(bucket, key, startMultipart.uploadId(), completeRq, len);
+    }
+
+    S3ObjectUploadResult putUnknownSizeBody(String bucket, String key, InputStreamS3Body body) throws IOException {
+        var buf = new byte[this.uploadPartSize];
+        // download first part
+        try (var is = body.asInputStream()) {
+            var read = is.readNBytes(buf, 0, buf.length);
+            if (read < buf.length) {
+                // body is less than one part
+                return this.putFullBody(bucket, key, body.encoding(), body.contentType(), buf, 0, read);
+            }
+            var startMultipartResult = this.startMultipartUpload(bucket, key, body.encoding(), body.contentType());
+            var completeRequest = new CompleteMultipartUploadRequest(new ArrayList<>());
+            var len = 0L;
+            try {
+                for (var i = 1; read > 0; i++) {
+                    len += len;
+                    try (var telemetry = this.telemetry.putObjectPart(bucket, key, startMultipartResult.uploadId(), i, read)) {
+                        try {
+                            var part = this.uploadPart(telemetry, bucket, key, startMultipartResult.uploadId(), i, buf, read);
+                            completeRequest.parts().add(part);
+                        } catch (Throwable t) {
+                            telemetry.setError(t);
+                            throw t;
+                        }
+                    }
+                    read = is.readNBytes(buf, 0, buf.length);
+                }
+            } catch (Throwable t) {
+                try {
+                    this.abortUpload(bucket, key, startMultipartResult.uploadId());
+                } catch (Throwable e) {
+                    t.addSuppressed(e);
+                }
+                throw t;
+            }
+            return this.completeUpload(bucket, key, startMultipartResult.uploadId(), completeRequest, len);
+        }
+    }
+
+    private S3ObjectUploadResult completeUpload(String bucket, String key, String uploadId, CompleteMultipartUploadRequest completeRequest, long objectSize) {
+        try (var telemetry = this.telemetry.completeMultipartUpload(bucket, key, uploadId)) {
+            var xml = completeRequest.toXml();
+            var sha256 = DigestUtils.sha256(xml, 0, xml.length).hex();
+            var uri = this.uriHelper.uri(bucket, key + "?uploadId=" + uploadId);
+            var request = HttpClientRequest.of("POST", uri, "/{bucket}?uploadId={uploadId}", HttpHeaders.of(), HttpBody.of(xml), this.requestTimeout);
+            var queryParams = new TreeMap();
+            queryParams.put("uploadId", uploadId);
+            var headers = Map.of(
+                "content-type", "text/xml",
+                "content-length", Integer.toString(xml.length),
+                "x-amz-mp-object-size", Long.toString(objectSize)
+            );
+            this.signer.processRequest(request, queryParams, headers, sha256);
+            try (var rs = this.httpClient.execute(request).toCompletableFuture().get();
+                 var body = rs.body()) {
+                telemetry.setAwsRequestId(rs.headers().getFirst("X-Amz-Request-Id"));
+                telemetry.setAwsExtendedId(rs.headers().getFirst("x-amz-id-2"));
+                if (rs.code() == 200) {
+                    var result = CompleteMultipartUploadResult.fromXml(body.asInputStream());
+                    var version = rs.headers().getFirst("x-amz-version-id");
+                    return new S3ObjectUploadResult(bucket, key, result.etag(), version);
+                }
+                throw S3ClientImpl.parseS3Exception(rs, body);
+            } catch (S3ClientException e) {
+                telemetry.setError(e);
+                throw e;
+            } catch (Exception e) {
+                telemetry.setError(e);
+                throw new S3ClientUnknownException(e);
+            }
+        }
+    }
+
+    private void abortUpload(String bucket, String key, String uploadId) {
+        try (var telemetry = this.telemetry.abortMultipartUpload(bucket, key, uploadId)) {
+            var uri = this.uriHelper.uri(bucket, key + "?uploadId=" + uploadId);
+            var request = HttpClientRequest.of("DELETE", uri, "/{bucket}?uploadId={uploadId}", HttpHeaders.of(), HttpBody.empty(), this.requestTimeout);
+            var queryParams = new TreeMap();
+            queryParams.put("uploadId", uploadId);
+            this.signer.processRequest(request, queryParams, Map.of(), S3RequestSigner.EMPTY_PAYLOAD_SHA256);
+            try (var rs = this.httpClient.execute(request).toCompletableFuture().get();
+                 var body = rs.body()) {
+                if (rs.code() == 204) {
+                    return;
+                }
+                throw S3ClientImpl.parseS3Exception(rs, body);
+            } catch (S3ClientException e) {
+                telemetry.setError(e);
+                throw e;
+            } catch (Exception e) {
+                telemetry.setError(e);
+                throw new S3ClientUnknownException(e);
+            }
+        }
+    }
+
+    private CompleteMultipartUploadRequest.Part uploadPart(S3Telemetry.S3TelemetryContext telemetry, String bucket, String key, String uploadId, int partNumber, byte[] buf, int read) {
+        var sha256 = DigestUtils.sha256(buf, 0, read);
+        var sha256Hex = sha256.hex();
+        var sha256Base64 = sha256.base64();
+        var md5 = DigestUtils.md5(buf, 0, read).base64();
+        var headers = Map.of(
+            "content-length", Integer.toString(read),
+            "content-md5", md5,
+            "x-amz-checksum-sha256", sha256Base64
+        );
+        var queryParams = new TreeMap();
+        queryParams.put("partNumber", Integer.toString(partNumber));
+        queryParams.put("uploadId", uploadId);
+        var uri = this.uriHelper.uri(bucket, key + "?partNumber=" + partNumber + "&uploadId=" + uploadId);
+        var httpBody = HttpBody.of(ByteBuffer.wrap(buf, 0, read));
+        var request = HttpClientRequest.of("PUT", uri, "/{bucket}/{key}?partNumber={partNumber}&uploadId={uploadId}", HttpHeaders.of(), httpBody, this.requestTimeout);
+        this.signer.processRequest(request, queryParams, headers, sha256Hex);
+        try (var rs = this.httpClient.execute(request).toCompletableFuture().get();
+             var body = rs.body()) {
+            telemetry.setAwsRequestId(rs.headers().getFirst("X-Amz-Request-Id"));
+            telemetry.setAwsExtendedId(rs.headers().getFirst("x-amz-id-2"));
+            if (rs.code() == 200) {
+                var etag = rs.headers().getFirst("ETag");
+                return new CompleteMultipartUploadRequest.Part(etag, partNumber, sha256Base64);
+            }
+            throw S3ClientImpl.parseS3Exception(rs, body);
+        } catch (S3ClientException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new S3ClientUnknownException(e);
+        }
+    }
+
+    private InitiateMultipartUploadResult startMultipartUpload(String bucket, String key, @Nullable String encoding, @Nullable String contentType) {
+        try (var telemetry = this.telemetry.startMultipartUpload(bucket, key)) {
+            var headers = HttpHeaders.of();
+            if (encoding != null) {
+                headers.add("content-encoding", encoding);
+            }
+            if (contentType != null) {
+                headers.add("content-type", contentType);
+            }
+            headers.add("x-amz-checksum-algorithm", "SHA256");
+            headers.add("x-amz-checksum-type", "COMPOSITE");
+            var uri = this.uriHelper.uri(bucket, key + "?uploads=true");
+            var request = HttpClientRequest.of("POST", uri, "/{bucket}?uploads=true", headers, HttpBody.empty(), this.requestTimeout);
+            this.signer.processRequest(request, new TreeMap<>(Map.of("uploads", "true")), Map.of(), S3RequestSigner.EMPTY_PAYLOAD_SHA256);
+            try (var rs = this.httpClient.execute(request).toCompletableFuture().get();
+                 var body = rs.body()) {
+                telemetry.setAwsRequestId(rs.headers().getFirst("X-Amz-Request-Id"));
+                telemetry.setAwsExtendedId(rs.headers().getFirst("x-amz-id-2"));
+                if (rs.code() == 200) {
+                    var result = InitiateMultipartUploadResult.fromXml(body.asInputStream());
+                    telemetry.setUploadId(result.uploadId());
+                    return result;
+                }
+                throw S3ClientImpl.parseS3Exception(rs, body);
+            } catch (S3ClientException e) {
+                telemetry.setError(e);
+                throw e;
+            } catch (Exception e) {
+                telemetry.setError(e);
+                throw new S3ClientUnknownException(e);
+            }
+        }
+    }
+
+    S3ObjectUploadResult putKnownSizeBody(String bucket, String key, String contentType, String contentEncoding, InputStream is, long size) {
+        var uri = this.uriHelper.uri(bucket, key);
+        try (var telemetry = this.telemetry.putObject(bucket, key, size)) {
+            try {
+                var result = putAwsChunked(telemetry, uri, S3RequestSigner.EMPTY_QUERY, size, contentType, contentEncoding, is, null);
+                return new S3ObjectUploadResult(bucket, key, result.etag(), result.versionId());
+            } catch (Throwable t) {
+                telemetry.setError(t);
+                throw t;
+            }
+        }
+    }
+
+    S3ObjectUploadResult putFullBody(String bucket, String key, String contentEncoding, String contentType, byte[] buf, int off, int len) {
+        try (var telemetry = this.telemetry.putObject(bucket, key, len)) {
+            var bodySha256 = DigestUtils.sha256(buf, off, len).hex();
+            var headers = new HashMap();
+            if (contentEncoding != null) {
+                headers.put("content-encoding", contentEncoding);
+            }
+            var uri = this.uriHelper.uri(bucket, key);
+            var httpBody = HttpBody.of(contentType, ByteBuffer.wrap(buf, off, len));
+            var request = HttpClientRequest.of("PUT", uri, "/{bucket}/{key}", HttpHeaders.of(), httpBody, this.requestTimeout);
+            this.signer.processRequest(request, S3RequestSigner.EMPTY_QUERY, headers, bodySha256);
+            try (var rs = this.httpClient.execute(request).toCompletableFuture().get();
+                 var body = rs.body()) {
+                telemetry.setAwsRequestId(rs.headers().getFirst("X-Amz-Request-Id"));
+                telemetry.setAwsExtendedId(rs.headers().getFirst("x-amz-id-2"));
+                if (rs.code() == 200) {
+                    return new S3ObjectUploadResult(
+                        bucket,
+                        key,
+                        rs.headers().getFirst("etag"),
+                        rs.headers().getFirst("x-amz-version-id")
+                    );
+                }
+                throw S3ClientImpl.parseS3Exception(rs, body);
+            } catch (S3ClientException e) {
+                telemetry.setError(e);
+                throw e;
+            } catch (Exception e) {
+                telemetry.setError(e);
+                throw new S3ClientUnknownException(e);
+            }
+        }
+    }
+
+    private static class LimitedInputStream extends InputStream {
+        protected final InputStream in;
+        private final int originalLength;
+        private int remaining;
+
+        private LimitedInputStream(InputStream in, int len) {
+            this.in = in;
+            this.originalLength = len;
+            this.remaining = len;
+        }
+
+        public int read() throws IOException {
+            if (this.remaining == 0) {
+                return -1;
+            }
+            var b = this.in.read();
+            if (b < 0) {
+                throw new EOFException("DEF length " + this.originalLength + " object truncated by " + this.remaining);
+            }
+            this.remaining--;
+            return b;
+        }
+
+        public int read(byte[] buf, int off, int len) throws IOException {
+            if (this.remaining == 0) {
+                return -1;
+            }
+            var toRead = Math.min(len, this.remaining);
+            var numRead = this.in.read(buf, off, toRead);
+            if (numRead < 0) {
+                throw new EOFException("DEF length " + this.originalLength + " object truncated by " + this.remaining);
+            }
+            this.remaining -= numRead;
+
+            return numRead;
+        }
+    }
+
+    public S3ObjectUploadResult putKnownSizeBodyMultiparted(String bucket, String key, @Nullable String contentType, @Nullable String contentEncoding, InputStream is, long size) {
+        var startMultipart = this.startMultipartUpload(bucket, key, contentEncoding, contentType);
+        var parts = new ArrayList();
+        try {
+            var bytesToUpload = size;
+            for (var i = 1; bytesToUpload > 0; i++) {
+                var partSize = bytesToUpload < this.singlePartLimit * 2L
+                    ? bytesToUpload
+                    : this.singlePartLimit;
+
+                var partNumber = Integer.toString(i);
+                var uri = this.uriHelper.uri(bucket, key + "?partNumber=" + partNumber + "&uploadId=" + startMultipart.uploadId());
+                var query = new TreeMap();
+                query.put("uploadId", startMultipart.uploadId());
+                query.put("partNumber", partNumber);
+                try (var telemetry = this.telemetry.putObjectPart(bucket, key, startMultipart.uploadId(), i, partSize)) {
+                    try {
+                        var part = this.putAwsChunked(telemetry, uri, query, partSize, contentType, contentEncoding, new LimitedInputStream(is, Math.toIntExact(partSize)), null);
+                        parts.add(new CompleteMultipartUploadRequest.Part(part.etag(), i, part.sha256()));
+                    } catch (Throwable t) {
+                        telemetry.setError(t);
+                        throw t;
+                    }
+                }
+                bytesToUpload -= partSize;
+            }
+        } catch (Throwable t) {
+            try {
+                this.abortUpload(bucket, key, startMultipart.uploadId());
+            } catch (Throwable e) {
+                t.addSuppressed(e);
+            }
+            throw t;
+        }
+        return this.completeUpload(bucket, key, startMultipart.uploadId(), new CompleteMultipartUploadRequest(parts), size);
+    }
+
+    record PutAwsChunkedResult(String etag, String sha256, @Nullable String versionId) {}
+
+    private PutAwsChunkedResult putAwsChunked(S3Telemetry.S3TelemetryContext telemetry, URI uri, SortedMap queryParams, long size, @Nullable String contentType, @Nullable String contentEncoding, InputStream is, @Nullable String sha256Base64) {
+        assert size >= 0;
+        var httpBody = new KnownSizeAwsChunkedHttpBody(this.signer, this.uploadChunkSize, this.uploadChunkSizeHex, contentType, is, size, sha256Base64);
+        var request = HttpClientRequest.of("PUT", uri, "/{bucket}/{key}", HttpHeaders.of(), httpBody, this.requestTimeout);
+        var signature = this.signer.processRequestChunked(httpBody.date, request, queryParams, size, httpBody.contentLength(), contentEncoding, sha256Base64);
+        httpBody.previousSignature = signature;
+
+        try (var rs = this.httpClient.execute(request).toCompletableFuture().get();
+             var body = rs.body()) {
+            telemetry.setAwsRequestId(rs.headers().getFirst("X-Amz-Request-Id"));
+            telemetry.setAwsExtendedId(rs.headers().getFirst("x-amz-id-2"));
+            if (rs.code() == 200) {
+                var etag = rs.headers().getFirst("etag");
+                return new PutAwsChunkedResult(
+                    etag,
+                    httpBody.sha256(),
+                    rs.headers().getFirst("x-amz-version-id")
+                );
+            }
+            throw S3ClientImpl.parseS3Exception(rs, body);
+        } catch (S3ClientException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new S3ClientUnknownException(e);
+        }
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3RequestSigner.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3RequestSigner.java
new file mode 100644
index 000000000..1cbdcdd58
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3RequestSigner.java
@@ -0,0 +1,277 @@
+package ru.tinkoff.kora.s3.client.impl;
+
+import jakarta.annotation.Nullable;
+import ru.tinkoff.kora.http.client.common.request.HttpClientRequest;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+public class S3RequestSigner {
+    public static final String EMPTY_PAYLOAD_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
+    public static final SortedMap EMPTY_QUERY = Collections.unmodifiableSortedMap(new TreeMap<>());
+    private static final ZoneId UTC = ZoneId.of("Z");
+    private static final DateTimeFormatter SIGNER_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd", Locale.US).withZone(UTC);
+    private static final DateTimeFormatter AMZ_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'", Locale.US).withZone(UTC);
+    private static final byte[] CLRF_BYTES = "\r\n".getBytes(StandardCharsets.US_ASCII);
+
+    private final String accessKey;
+    private final Mac secretKey;
+    private final String region;
+
+    public S3RequestSigner(String accessKey, String secretKey, String region) {
+        this.accessKey = accessKey;
+        this.region = region;
+        var secretKeySpec = new SecretKeySpec(("AWS4" + secretKey).getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+        try {
+            this.secretKey = Mac.getInstance("HmacSHA256");
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalStateException(e); // never gonna happen
+        }
+        try {
+            this.secretKey.init(secretKeySpec);
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+
+    public void processRequest(HttpClientRequest request, SortedMap queryParameters, Map additionalHeaders, String payloadSha256) {
+        var date = ZonedDateTime.now(ZoneOffset.UTC);
+        var amzDate = date.format(AMZ_DATE_FORMAT);
+
+        final CharSequence signedHeaders;
+        final CharSequence canonicalHeadersStr;
+        if (additionalHeaders.isEmpty()) {
+            signedHeaders = "host;x-amz-content-sha256;x-amz-date";
+            canonicalHeadersStr = "host:" + request.uri().getAuthority() + "\n"
+                + "x-amz-content-sha256:" + payloadSha256 + "\n"
+                + "x-amz-date:" + amzDate + "\n";
+        } else {
+            var headers = new TreeMap();
+            headers.put("host", request.uri().getAuthority());
+            headers.put("x-amz-date", amzDate);
+            for (var entry : additionalHeaders.entrySet()) {
+                var headerName = entry.getKey().toLowerCase();
+                headers.put(headerName, entry.getValue());
+                request.headers().set(headerName, entry.getValue());
+            }
+            var signedHeadersSb = new StringBuilder();
+            var canonicalHeadersStrSb = new StringBuilder();
+            for (var it = headers.entrySet().iterator(); it.hasNext(); ) {
+                var entry = it.next();
+                var header = entry.getKey();
+                var value = entry.getValue();
+                signedHeadersSb.append(header);
+                if (it.hasNext()) {
+                    signedHeadersSb.append(';');
+                }
+                canonicalHeadersStrSb.append(header).append(':').append(value).append('\n');
+            }
+            signedHeaders = signedHeadersSb;
+            canonicalHeadersStr = canonicalHeadersStrSb;
+        }
+
+
+        var canonicalQueryStr = getCanonicalizedQueryString(queryParameters);
+        // CanonicalRequest =
+        //   HTTPRequestMethod + '\n' +
+        //   CanonicalURI + '\n' +
+        //   CanonicalQueryString + '\n' +
+        //   CanonicalHeaders + '\n' +
+        //   SignedHeaders + '\n' +
+        //   HexEncode(Hash(RequestPayload))
+        var canonicalRequest = request.method() + "\n"
+            + request.uri().getPath() + "\n"
+            + canonicalQueryStr + "\n"
+            + canonicalHeadersStr + "\n"
+            + signedHeaders + "\n"
+            + payloadSha256;
+
+
+        var signerDate = date.format(SIGNER_DATE_FORMAT);
+        var scope = signerDate + "/" + this.region + "/s3/aws4_request";
+        var canonicalRequestHash = DigestUtils.sha256(canonicalRequest).hex();
+
+        var stringToSign = "AWS4-HMAC-SHA256" + "\n"
+            + amzDate + "\n"
+            + scope + "\n"
+            + canonicalRequestHash;
+
+        var signature = this.awsSign(signerDate, stringToSign);
+        var authorization = "AWS4-HMAC-SHA256 Credential=" + accessKey + "/" + scope + ", SignedHeaders=" + signedHeaders + ", Signature=" + signature;
+
+        request.headers().set("host", request.uri().getAuthority());
+        request.headers().set("x-amz-date", amzDate);
+        request.headers().set("authorization", authorization);
+        request.headers().set("x-amz-content-sha256", payloadSha256);
+    }
+
+    public String processRequestChunked(ZonedDateTime date, HttpClientRequest request, SortedMap queryParameters, long bodyLength, long contentLength, @Nullable String contentEncoding, @Nullable String sha256Base64) {
+        var amzDate = date.format(AMZ_DATE_FORMAT);
+        if (contentEncoding == null) {
+            contentEncoding = "aws-chunked";
+        } else {
+            contentEncoding = "aws-chunked, " + contentEncoding;
+        }
+
+        var signedHeaders = sha256Base64 == null
+            ? "content-encoding;content-length;expect;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-trailer"
+            : "content-encoding;content-length;expect;host;x-amz-checksum-sha256;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length";
+        var canonicalHeadersStr = "content-encoding:" + contentEncoding + "\n"
+            + "content-length:" + contentLength + "\n"
+            + "expect:100-continue\n"
+            + "host:" + request.uri().getAuthority() + "\n"
+            + (sha256Base64 == null ? "" : "x-amz-checksum-sha256:" + sha256Base64 + "\n")
+            + (sha256Base64 == null ? "x-amz-content-sha256:STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER" : "x-amz-content-sha256:STREAMING-AWS4-HMAC-SHA256-PAYLOAD") + "\n"
+            + "x-amz-date:" + amzDate + "\n"
+            + "x-amz-decoded-content-length:" + bodyLength + "\n"
+            + (sha256Base64 != null ? "" : "x-amz-trailer:x-amz-checksum-sha256\n");
+
+        var canonicalQueryStr = getCanonicalizedQueryString(queryParameters);
+        var canonicalRequest = request.method() + "\n"
+            + request.uri().getPath() + "\n"
+            + canonicalQueryStr + "\n"
+            + canonicalHeadersStr + "\n"
+            + signedHeaders + "\n"
+            + (sha256Base64 != null ? "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" : "STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER");
+        var canonicalRequestHash = DigestUtils.sha256(canonicalRequest).hex();
+
+        var signerDate = date.format(SIGNER_DATE_FORMAT);
+        var scope = signerDate + "/" + this.region + "/s3/aws4_request";
+
+        var stringToSign = "AWS4-HMAC-SHA256" + "\n"
+            + amzDate + "\n"
+            + scope + "\n"
+            + canonicalRequestHash;
+
+        var signature = this.awsSign(signerDate, stringToSign);
+        var authorization = "AWS4-HMAC-SHA256 Credential=" + accessKey + "/" + scope + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature;
+
+        request.headers().set("content-encoding", contentEncoding);
+        request.headers().set("host", request.uri().getAuthority());
+        if (sha256Base64 != null) {
+            request.headers().set("x-amz-checksum-sha256", sha256Base64);
+            request.headers().set("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD");
+        } else {
+            request.headers().set("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER");
+            request.headers().set("x-amz-trailer", "x-amz-checksum-sha256");
+        }
+        request.headers().set("x-amz-date", amzDate);
+        request.headers().set("x-amz-decoded-content-length", bodyLength);
+        request.headers().set("authorization", authorization);
+        request.headers().set("expect", "100-continue");
+
+        return signature;
+    }
+
+    public String processChunk(ZonedDateTime date, String previousSignature, byte[] buf, int read, OutputStream os) throws IOException {
+        var amzDate = date.format(AMZ_DATE_FORMAT);
+        var signerDate = date.format(SIGNER_DATE_FORMAT);
+
+        var scope = signerDate + "/" + this.region + "/s3/aws4_request";
+        var chunkContentSha = DigestUtils.sha256(buf, 0, read).hex();
+
+        var stringToSign = "AWS4-HMAC-SHA256-PAYLOAD" + "\n"
+            + amzDate + "\n"
+            + scope + "\n"
+            + previousSignature + "\n"
+            + EMPTY_PAYLOAD_SHA256 + "\n"
+            + chunkContentSha;
+
+        var signature = this.awsSign(signerDate, stringToSign);
+
+        os.write(Integer.toHexString(read).getBytes(StandardCharsets.US_ASCII));
+        os.write(";chunk-signature=".getBytes(StandardCharsets.US_ASCII));
+        os.write(signature.getBytes(StandardCharsets.US_ASCII));
+        os.write(CLRF_BYTES);
+        os.write(buf, 0, read);
+        os.write(CLRF_BYTES);
+
+        return signature;
+    }
+
+    public String processFinalChunk(ZonedDateTime date, String previousSignature, OutputStream os) throws IOException {
+        var amzDate = date.format(AMZ_DATE_FORMAT);
+        var signerDate = date.format(SIGNER_DATE_FORMAT);
+        var scope = signerDate + "/" + this.region + "/s3/aws4_request";
+
+        var stringToSign = "AWS4-HMAC-SHA256-PAYLOAD" + "\n"
+            + amzDate + "\n"
+            + scope + "\n"
+            + previousSignature + "\n"
+            + EMPTY_PAYLOAD_SHA256 + "\n"
+            + EMPTY_PAYLOAD_SHA256;
+
+        var signature = this.awsSign(signerDate, stringToSign);
+
+        os.write("0".getBytes(StandardCharsets.US_ASCII));
+        os.write(";chunk-signature=".getBytes(StandardCharsets.US_ASCII));
+        os.write(signature.getBytes(StandardCharsets.US_ASCII));
+        os.write(CLRF_BYTES);
+
+        return signature;
+    }
+
+    public String processTrailer(ZonedDateTime date, String previousSignature, String sha256, OutputStream os) throws IOException {
+        var amzDate = date.format(AMZ_DATE_FORMAT);
+        var signerDate = date.format(SIGNER_DATE_FORMAT);
+
+        var scope = signerDate + "/" + this.region + "/s3/aws4_request";
+        var trailerBytes = ("x-amz-checksum-sha256:" + sha256 + "\n").getBytes(StandardCharsets.US_ASCII);
+        var chunkContentSha = DigestUtils.sha256(trailerBytes, 0, trailerBytes.length).hex();
+
+        var stringToSign = "AWS4-HMAC-SHA256-TRAILER" + "\n"
+            + amzDate + "\n"
+            + scope + "\n"
+            + previousSignature + "\n"
+            + chunkContentSha;
+
+        var signature = this.awsSign(signerDate, stringToSign);
+        os.write(trailerBytes);
+        os.write(CLRF_BYTES);
+        os.write("x-amz-trailer-signature:".getBytes(StandardCharsets.US_ASCII));
+        os.write(signature.getBytes(StandardCharsets.US_ASCII));
+        os.write(CLRF_BYTES);
+        os.write(CLRF_BYTES);
+
+        return signature;
+    }
+
+    private String awsSign(String signerDate, String stringToSign) {
+        var dateKey = DigestUtils.sumHmac(secretKey, signerDate.getBytes(StandardCharsets.US_ASCII));
+        var dateRegionKey = DigestUtils.sumHmac(dateKey, region.getBytes(StandardCharsets.US_ASCII));
+        var dateRegionServiceKey = DigestUtils.sumHmac(dateRegionKey, "s3".getBytes(StandardCharsets.US_ASCII));
+        var signingKey = DigestUtils.sumHmac(dateRegionServiceKey, "aws4_request".getBytes(StandardCharsets.US_ASCII));
+        var digest = DigestUtils.sumHmac(signingKey, stringToSign.getBytes(StandardCharsets.UTF_8));
+        return HexFormat.of().formatHex(digest);
+    }
+
+
+    private static String getCanonicalizedQueryString(SortedMap parameters) {
+        if (parameters == null || parameters.isEmpty()) {
+            return "";
+        }
+
+        var builder = new StringBuilder();
+        for (var it = parameters.entrySet().iterator(); it.hasNext(); ) {
+            var pair = it.next();
+            builder.append(pair.getKey());
+            builder.append("=");
+            builder.append(pair.getValue());
+            if (it.hasNext()) {
+                builder.append("&");
+            }
+        }
+        return builder.toString();
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/UriHelper.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/UriHelper.java
new file mode 100644
index 000000000..5b9652f1b
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/UriHelper.java
@@ -0,0 +1,43 @@
+package ru.tinkoff.kora.s3.client.impl;
+
+import ru.tinkoff.kora.s3.client.S3Config;
+
+import java.net.URI;
+import java.util.Objects;
+
+public class UriHelper {
+    private final S3Config.AddressStyle addressStyle;
+    private final String scheme;
+    private final String endpoint;
+
+    public UriHelper(S3Config config) {
+        var addressStyle = config.addressStyle();
+        if (addressStyle == null) {
+            throw new NullPointerException("addressStyle is null");
+        }
+        this.addressStyle = addressStyle;
+        var uri = URI.create(config.endpoint());
+        this.scheme = Objects.requireNonNullElse(uri.getScheme(), "https");
+        var endpoint = uri.getHost();
+        if (uri.getPort() != -1) {
+            endpoint += ":" + uri.getPort();
+        }
+        if (uri.getPath() != null && !uri.getRawPath().isBlank()) {
+            endpoint += "/" + uri.getRawPath();
+        }
+        if (endpoint.endsWith("/")) {
+            endpoint = endpoint.substring(0, endpoint.length() - 1);
+        }
+        this.endpoint = endpoint;
+    }
+
+    public URI uri(String bucket, String path) {
+        if (addressStyle == S3Config.AddressStyle.PATH) {
+            return URI.create(scheme + "://" + endpoint + "/" + bucket + "/" + path);
+        }
+        if (addressStyle == S3Config.AddressStyle.VIRTUAL_HOSTED) {
+            return URI.create(scheme + "://" + bucket + "." + endpoint + "/" + path);
+        }
+        throw new IllegalStateException("AddressStyle is not supported: " + addressStyle);
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/CompleteMultipartUploadRequest.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/CompleteMultipartUploadRequest.java
new file mode 100644
index 000000000..56d0e3528
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/CompleteMultipartUploadRequest.java
@@ -0,0 +1,66 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import jakarta.annotation.Nullable;
+
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.util.List;
+
+//
+//   
+//      string
+//      string
+//      string
+//      string
+//      string
+//      string
+//      integer
+//   
+//   ...
+//
+public record CompleteMultipartUploadRequest(List parts) {
+    public record Part(String etag, int partNumber, @Nullable String checksumSha256) {}
+
+    public byte[] toXml() {
+        try (var baos = new ByteArrayOutputStream()) {
+            writeXml(baos);
+            baos.flush();
+            return baos.toByteArray();
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        } catch (XMLStreamException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    void writeXml(OutputStream os) throws IOException, XMLStreamException {
+        var xml = XMLOutputFactory.newDefaultFactory().createXMLStreamWriter(os, "UTF-8");
+        try {
+            xml.writeStartDocument("UTF-8", "1.0");
+            xml.writeStartElement("CompleteMultipartUpload");
+            xml.writeAttribute("xmlns", "http://s3.amazonaws.com/doc/2006-03-01/");
+            for (var part : this.parts) {
+                xml.writeStartElement("Part");
+                xml.writeStartElement("ChecksumSHA256");
+                xml.writeCharacters(part.checksumSha256());
+                xml.writeEndElement();
+                xml.writeStartElement("ETag");
+                xml.writeCharacters(part.etag());
+                xml.writeEndElement();
+                xml.writeStartElement("PartNumber");
+                xml.writeCharacters(Integer.toString(part.partNumber()));
+                xml.writeEndElement();
+                xml.writeEndElement();
+            }
+            xml.writeEndElement();
+            xml.writeEndDocument();
+            xml.flush();
+        } finally {
+            xml.close();
+        }
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/CompleteMultipartUploadResult.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/CompleteMultipartUploadResult.java
new file mode 100644
index 000000000..fa500ae34
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/CompleteMultipartUploadResult.java
@@ -0,0 +1,33 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+import java.io.IOException;
+import java.io.InputStream;
+
+//
+//
+//   string
+//   string
+//   string
+//   string
+//   string
+//   string
+//   string
+//   string
+//   string
+//   string
+//
+public record CompleteMultipartUploadResult(String location, String bucket, String key, String etag) {
+    public static CompleteMultipartUploadResult fromXml(InputStream is) throws ParserConfigurationException, SAXException, IOException {
+        var handler = new CompleteMultipartUploadResultSaxHandler();
+        SAXParserFactory.newDefaultInstance()
+            .newSAXParser()
+            .parse(is, handler);
+
+        return handler.toResult();
+    }
+
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/CompleteMultipartUploadResultSaxHandler.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/CompleteMultipartUploadResultSaxHandler.java
new file mode 100644
index 000000000..e89f91a38
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/CompleteMultipartUploadResultSaxHandler.java
@@ -0,0 +1,64 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+public class CompleteMultipartUploadResultSaxHandler extends DefaultHandler {
+    private int level = 0;
+    private final StringBuilder buf = new StringBuilder();
+
+    private String location;
+    private String bucket;
+    private String key;
+    private String etag;
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+        level++;
+        if (level == 1) {
+            if (!qName.equals("CompleteMultipartUploadResult")) {
+                throw new SAXException("Invalid element '" + qName + "'. Expected 'CompleteMultipartUploadResult'");
+            }
+        }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        level--;
+        if (level > 1) {
+            return;
+        }
+        if (level == 1) {
+            switch (qName) {
+                case "Location":
+                    location = buf.toString();
+                    break;
+                case "Bucket":
+                    bucket = buf.toString();
+                    break;
+                case "Key":
+                    key = buf.toString();
+                    break;
+                case "ETag":
+                    etag = buf.toString();
+                    break;
+                default:
+                    break;
+            }
+            buf.setLength(0);
+        }
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        if (level > 1) {
+            return;
+        }
+        buf.append(ch, start, length);
+    }
+
+    public CompleteMultipartUploadResult toResult() {
+        return new CompleteMultipartUploadResult(location, bucket, key, etag);
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsRequest.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsRequest.java
new file mode 100644
index 000000000..9f168eb10
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsRequest.java
@@ -0,0 +1,62 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.util.List;
+
+//
+//   
+//      string
+//      string
+//      timestamp
+//      long
+//      string
+//   
+//   ...
+//   boolean
+//
+public record DeleteObjectsRequest(List objects) {
+    public record S3Object(String key) {
+        public void writeXml(XMLStreamWriter xml) throws XMLStreamException {
+            xml.writeStartElement("Object");
+            xml.writeStartElement("Key");
+            xml.writeCharacters(key);
+            xml.writeEndElement();
+            xml.writeEndElement();
+        }
+    }
+
+    public static byte[] toXml(Iterable objects) {
+        try (var baos = new ByteArrayOutputStream()) {
+            toXml(objects, baos);
+            baos.flush();
+            return baos.toByteArray();
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        } catch (XMLStreamException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static void toXml(Iterable objects, OutputStream os) throws XMLStreamException {
+        var xml = XMLOutputFactory.newDefaultFactory().createXMLStreamWriter(os, "UTF-8");
+        try {
+            xml.writeStartDocument("UTF-8", "1.0");
+            xml.writeStartElement("Delete");
+            xml.writeAttribute("xmlns", "http://s3.amazonaws.com/doc/2006-03-01/");
+            for (var object : objects) {
+                object.writeXml(xml);
+            }
+            xml.writeEndElement();
+            xml.writeEndDocument();
+            xml.flush();
+        } finally {
+            xml.close();
+        }
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsResult.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsResult.java
new file mode 100644
index 000000000..a9fc44980
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsResult.java
@@ -0,0 +1,24 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+public record DeleteObjectsResult(List deleted) {
+    public record Deleted(String key) {}
+
+    public static DeleteObjectsResult fromXml(InputStream is) throws ParserConfigurationException, SAXException, IOException {
+        var handler = new DeleteObjectsResultSaxHandler();
+        SAXParserFactory.newDefaultInstance()
+            .newSAXParser()
+            .parse(is, handler);
+
+        return handler.toResult();
+    }
+
+}
+
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsResultSaxHandler.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsResultSaxHandler.java
new file mode 100644
index 000000000..10f8ce2d8
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsResultSaxHandler.java
@@ -0,0 +1,131 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DeleteObjectsResultSaxHandler extends DefaultHandler {
+    private int level = 0;
+    private final StringBuilder buf = new StringBuilder();
+    private final List deleted = new ArrayList<>();
+
+    private DefaultHandler delegate = null;
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+        level++;
+        if (level == 1) {
+            if (!qName.equals("DeleteResult")) {
+                throw new SAXException("Expected , got " + qName);
+            }
+        }
+        if (level > 2 && delegate != null) {
+            delegate.startElement(uri, localName, qName, attributes);
+            return;
+        }
+        if (level == 2) {
+            switch (qName) {
+                case "Deleted":
+                    delegate = new DeleteObjectResultDeletedSaxHandler(buf);
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        level--;
+        if (level > 1 && delegate != null) {
+            delegate.endElement(uri, localName, qName);
+            return;
+        }
+        if (level == 1) {
+            switch (qName) {
+                case "Deleted":
+                    assert delegate instanceof DeleteObjectResultDeletedSaxHandler;
+                    deleted.add(((DeleteObjectResultDeletedSaxHandler) delegate).toResult());
+                    delegate = null;
+                    break;
+                default:
+                    break;
+            }
+            buf.setLength(0);
+        }
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        if (level > 1) {
+            if (delegate != null) {
+                delegate.characters(ch, start, length);
+            }
+            return;
+        }
+        buf.append(ch, start, length);
+    }
+
+    public DeleteObjectsResult toResult() {
+        return new DeleteObjectsResult(deleted);
+    }
+
+    public static class DeleteObjectResultDeletedSaxHandler extends DefaultHandler {
+        private int level = 0;
+        private final StringBuilder buf;
+
+        private String key;
+
+        public DeleteObjectResultDeletedSaxHandler(StringBuilder buf) {
+            this.buf = buf;
+            buf.setLength(0);
+        }
+
+        @Override
+        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+            level++;
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String qName) throws SAXException {
+            level--;
+            if (level > 0) {
+                return;
+            }
+            if (level == 0) {
+                switch (qName) {
+                    case "Key":
+                        key = buf.toString();
+                        break;
+                    default:
+                        break;
+                }
+                buf.setLength(0);
+            }
+
+        }
+
+        @Override
+        public void characters(char[] ch, int start, int length) throws SAXException {
+            if (level == 1) {
+                while (Character.isSpaceChar(ch[start]) && length > 0) {
+                    start++;
+                    length--;
+                }
+                while (Character.isSpaceChar(ch[start + length]) && length > 0) {
+                    length--;
+                }
+                if (level > 0) {
+                    buf.append(ch, start, length);
+                }
+            }
+        }
+
+        public DeleteObjectsResult.Deleted toResult() {
+            return new DeleteObjectsResult.Deleted(key);
+        }
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/InitiateMultipartUploadResult.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/InitiateMultipartUploadResult.java
new file mode 100644
index 000000000..4efeee66d
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/InitiateMultipartUploadResult.java
@@ -0,0 +1,25 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+import java.io.IOException;
+import java.io.InputStream;
+
+//
+//   string
+//   string
+//   string
+//
+public record InitiateMultipartUploadResult(String bucket, String key, String uploadId) {
+
+    public static InitiateMultipartUploadResult fromXml(InputStream is) throws ParserConfigurationException, SAXException, IOException {
+        var handler = new InitiateMultipartUploadResultSaxHandler();
+        SAXParserFactory.newDefaultInstance()
+            .newSAXParser()
+            .parse(is, handler);
+
+        return handler.toResult();
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/InitiateMultipartUploadResultSaxHandler.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/InitiateMultipartUploadResultSaxHandler.java
new file mode 100644
index 000000000..e0f681609
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/InitiateMultipartUploadResultSaxHandler.java
@@ -0,0 +1,58 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+public class InitiateMultipartUploadResultSaxHandler extends DefaultHandler {
+    private int level = 0;
+    private final StringBuilder buf = new StringBuilder();
+
+    private String bucket;
+    private String key;
+    private String uploadId;
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+        level++;
+        if (level == 1) {
+            if (!qName.equals("InitiateMultipartUploadResult")) {
+                throw new SAXException("Wrong element: " + qName + " ; expected ");
+            }
+        }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        level--;
+        if (level != 1) {
+            return;
+        }
+        switch (qName) {
+            case "Bucket":
+                bucket = buf.toString();
+                break;
+            case "Key":
+                key = buf.toString();
+                break;
+            case "UploadId":
+                uploadId = buf.toString();
+                break;
+            default:
+                break;
+        }
+        buf.setLength(0);
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        if (level == 2) {
+            buf.append(ch, start, length);
+        }
+    }
+
+    public InitiateMultipartUploadResult toResult() {
+        return new InitiateMultipartUploadResult(bucket, key, uploadId);
+    }
+
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/ListBucketResult.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/ListBucketResult.java
new file mode 100644
index 000000000..089fd4670
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/ListBucketResult.java
@@ -0,0 +1,23 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import jakarta.annotation.Nullable;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+public record ListBucketResult(String name, String prefix, int keyCount, int maxKeys, String delimiter, boolean isTruncated, List contents, @Nullable String nextContinuationToken) {
+    public record Content(String key, String lastModified, String eTag, long size, String storageClass) {}
+
+    public static ListBucketResult fromXml(InputStream is) throws ParserConfigurationException, SAXException, IOException {
+        var handler = new ListBucketResultSaxHandler();
+        SAXParserFactory.newDefaultInstance()
+            .newSAXParser()
+            .parse(is, handler);
+
+        return handler.toResult();
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/ListBucketResultSaxHandler.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/ListBucketResultSaxHandler.java
new file mode 100644
index 000000000..1c7e1533e
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/ListBucketResultSaxHandler.java
@@ -0,0 +1,168 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ListBucketResultSaxHandler extends DefaultHandler {
+    private int level = 0;
+    private final StringBuilder buf = new StringBuilder();
+
+    private String name;
+    private String prefix;
+    private int keyCount;
+    private int maxKeys;
+    private String delimiter;
+    private boolean isTruncated;
+    private List contents;
+    private String nextContinuationToken;
+
+    private DefaultHandler delegate = null;
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+        level++;
+        if (level > 2 && delegate != null) {
+            delegate.startElement(uri, localName, qName, attributes);
+            return;
+        }
+        if (level == 2) {
+            switch (qName) {
+                case "Contents":
+                    delegate = new ListBucketResultContentSaxHandler(buf);
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        level--;
+        if (level > 1 && delegate != null) {
+            delegate.endElement(uri, localName, qName);
+            return;
+        }
+        if (level == 1) {
+            switch (qName) {
+                case "Name":
+                    name = buf.toString();
+                    break;
+                case "Prefix":
+                    prefix = buf.toString();
+                    break;
+                case "KeyCount":
+                    keyCount = Integer.parseInt(buf.toString());
+                    break;
+                case "MaxKeys":
+                    maxKeys = Integer.parseInt(buf.toString());
+                    break;
+                case "Delimiter":
+                    delimiter = buf.toString();
+                    break;
+                case "IsTruncated":
+                    isTruncated = Boolean.parseBoolean(buf.toString());
+                    break;
+                case "NextContinuationToken":
+                    nextContinuationToken = buf.toString();
+                    break;
+                case "Contents":
+                    assert delegate instanceof ListBucketResultContentSaxHandler;
+                    if (contents == null) {
+                        contents = new ArrayList<>();
+                    }
+                    contents.add(((ListBucketResultContentSaxHandler) delegate).toResult());
+                    delegate = null;
+                    break;
+                default:
+                    break;
+            }
+            buf.setLength(0);
+        }
+
+
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        if (level > 2) {
+            if (delegate != null) {
+                delegate.characters(ch, start, length);
+            }
+            return;
+        }
+        assert level >= 1 : "Expected level>=1, got level=" + level;
+        buf.append(ch, start, length);
+    }
+
+    public ListBucketResult toResult() {
+        return new ListBucketResult(name, prefix, keyCount, maxKeys, delimiter, isTruncated, contents, nextContinuationToken);
+    }
+
+    public static class ListBucketResultContentSaxHandler extends DefaultHandler {
+        private int level = 0;
+        private final StringBuilder buf;
+
+        private String key;
+        private String lastModified;
+        private String eTag;
+        private long size;
+        private String storageClass;
+
+        public ListBucketResultContentSaxHandler(StringBuilder buf) {
+            this.buf = buf;
+            buf.setLength(0);
+        }
+
+        @Override
+        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+            level++;
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String qName) throws SAXException {
+            level--;
+            if (level > 0) {
+                return;
+            }
+            if (level == 0) {
+                switch (qName) {
+                    case "Key":
+                        key = buf.toString();
+                        break;
+                    case "LastModified":
+                        lastModified = buf.toString();
+                        break;
+                    case "ETag":
+                        eTag = buf.toString();
+                        break;
+                    case "Size":
+                        size = Long.parseLong(buf.toString());
+                        break;
+                    case "StorageClass":
+                        storageClass = buf.toString();
+                        break;
+                    default:
+                        break;
+                }
+                buf.setLength(0);
+            }
+
+        }
+
+        @Override
+        public void characters(char[] ch, int start, int length) throws SAXException {
+            if (level == 1) {
+                buf.append(ch, start, length);
+            }
+        }
+
+        public ListBucketResult.Content toResult() {
+            return new ListBucketResult.Content(key, lastModified, eTag, size, storageClass);
+        }
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3Error.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3Error.java
new file mode 100644
index 000000000..3d0e20251
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3Error.java
@@ -0,0 +1,29 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import jakarta.annotation.Nullable;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+import java.io.IOException;
+import java.io.InputStream;
+
+public record S3Error(
+    String code,
+    String message,
+    @Nullable String key,
+    @Nullable String bucketName,
+    @Nullable String resource,
+    String requestId,
+    @Nullable String hostId
+) {
+
+    public static S3Error fromXml(InputStream is) throws ParserConfigurationException, SAXException, IOException {
+        var handler = new S3ErrorSaxHandler();
+        SAXParserFactory.newDefaultInstance()
+            .newSAXParser()
+            .parse(is, handler);
+
+        return handler.toResult();
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3ErrorSaxHandler.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3ErrorSaxHandler.java
new file mode 100644
index 000000000..32894fdab
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3ErrorSaxHandler.java
@@ -0,0 +1,83 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+public class S3ErrorSaxHandler extends DefaultHandler {
+    private final StringBuilder buf = new StringBuilder();
+    private int depth = 0;
+
+    @Override
+    public void endDocument() throws SAXException {
+        super.endDocument();
+    }
+
+    private String code;
+    private String message;
+    private String key;
+    private String bucketName;
+    private String resource;
+    private String requestId;
+    private String hostId;
+
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+        depth++;
+        if (depth == 1 && !qName.equals("Error")) {
+            throw new SAXException("Invalid response");
+        }
+        this.buf.setLength(0);
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        depth--;
+        if (depth == 1) {
+            switch (qName) {
+                case "Code":
+                    code = buf.toString();
+                    break;
+                case "Message":
+                    message = buf.toString();
+                    break;
+                case "Key":
+                    key = buf.toString();
+                    break;
+                case "BucketName":
+                    bucketName = buf.toString();
+                    break;
+                case "Resource":
+                    resource = buf.toString();
+                    break;
+                case "RequestId":
+                    requestId = buf.toString();
+                    break;
+                case "HostId":
+                    hostId = buf.toString();
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        this.buf.append(ch, start, length);
+    }
+
+    @Override
+    public void error(SAXParseException e) throws SAXException {
+        throw e;
+    }
+
+    public S3Error toResult() throws SAXException {
+        if (code == null || message == null) {
+            throw new SAXException("Invalid response");
+        }
+        return new S3Error(code, message, key, bucketName, resource, requestId, hostId);
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3Body.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3Body.java
new file mode 100644
index 000000000..75205e04b
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3Body.java
@@ -0,0 +1,70 @@
+package ru.tinkoff.kora.s3.client.model;
+
+import jakarta.annotation.Nullable;
+import org.jetbrains.annotations.ApiStatus;
+import ru.tinkoff.kora.s3.client.impl.ByteArrayS3Body;
+import ru.tinkoff.kora.s3.client.impl.InputStreamS3Body;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * S3 Object value representation
+ */
+@ApiStatus.Experimental
+public sealed interface S3Body extends Closeable permits ByteArrayS3Body, InputStreamS3Body {
+
+    default byte[] asBytes() throws IOException {
+        try (var stream = asInputStream()) {
+            return stream.readAllBytes();
+        }
+    }
+
+    InputStream asInputStream();
+
+    long size();
+
+    @Nullable
+    String encoding();
+
+    @Nullable
+    String contentType();
+
+    static S3Body ofBytes(byte[] body) {
+        return new ByteArrayS3Body(body, 0, body.length, "application/octet-stream", null);
+    }
+
+    static S3Body ofBytes(byte[] body, @Nullable String type) {
+        return new ByteArrayS3Body(body, 0, body.length, type, null);
+    }
+
+    static S3Body ofBytes(byte[] body, @Nullable String type, @Nullable String encoding) {
+        return new ByteArrayS3Body(body, 0, body.length, type, encoding);
+    }
+
+    static S3Body ofBytes(byte[] body, int offset, int length) {
+        return new ByteArrayS3Body(body, offset, length, "application/octet-stream", null);
+    }
+
+    static S3Body ofBytes(byte[] body, int offset, int length, @Nullable String type) {
+        return new ByteArrayS3Body(body, offset, length, type, null);
+    }
+
+    static S3Body ofBytes(byte[] body, int offset, int length, @Nullable String type, @Nullable String encoding) {
+        return new ByteArrayS3Body(body, offset, length, type, encoding);
+    }
+
+    static S3Body ofInputStream(InputStream inputStream, long size) {
+        return new InputStreamS3Body(inputStream, size, null, null);
+    }
+
+    static S3Body ofInputStream(InputStream inputStream, long size, @Nullable String type) {
+        return new InputStreamS3Body(inputStream, size, type, null);
+    }
+
+    static S3Body ofInputStream(InputStream inputStream, long size, @Nullable String type, @Nullable String encoding) {
+        return new InputStreamS3Body(inputStream, size, type, encoding);
+    }
+
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3Object.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3Object.java
new file mode 100644
index 000000000..51deac9c2
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3Object.java
@@ -0,0 +1,17 @@
+package ru.tinkoff.kora.s3.client.model;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * S3 Object representation
+ */
+@ApiStatus.Experimental
+public record S3Object(S3ObjectMeta meta, S3Body body) implements Closeable {
+    @Override
+    public void close() throws IOException {
+        this.body.close();
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectMeta.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectMeta.java
new file mode 100644
index 000000000..4be22382e
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectMeta.java
@@ -0,0 +1,18 @@
+package ru.tinkoff.kora.s3.client.model;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * S3 Object metadata representation
+ */
+@ApiStatus.Experimental
+public record S3ObjectMeta(String bucket, String key, Instant modified, long size) {
+    public S3ObjectMeta {
+        Objects.requireNonNull(bucket);
+        Objects.requireNonNull(key);
+        Objects.requireNonNull(modified);
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectUploadResult.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectUploadResult.java
new file mode 100644
index 000000000..308db9b92
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/model/S3ObjectUploadResult.java
@@ -0,0 +1,6 @@
+package ru.tinkoff.kora.s3.client.model;
+
+public record S3ObjectUploadResult(String bucket, String key, String etag, String versionId) {
+    public S3ObjectUploadResult {
+    }
+}
diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/package-info.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/package-info.java
similarity index 100%
rename from experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/package-info.java
rename to experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/package-info.java
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3Logger.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3Logger.java
new file mode 100644
index 000000000..48c518542
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3Logger.java
@@ -0,0 +1,47 @@
+package ru.tinkoff.kora.s3.client.telemetry;
+
+import jakarta.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ru.tinkoff.kora.logging.common.arg.StructuredArgument;
+
+public class DefaultS3Logger implements S3Logger {
+
+    private final Logger requestLogger;
+    private final Logger responseLogger;
+
+    public DefaultS3Logger(Class client) {
+        this.requestLogger = LoggerFactory.getLogger(client.getCanonicalName() + ".request");
+        this.responseLogger = LoggerFactory.getLogger(client.getCanonicalName() + ".response");
+    }
+
+    @Override
+    public void logRequest(String method, String bucket, @Nullable String key, @Nullable Long contentLength) {
+        if (!requestLogger.isInfoEnabled()) {
+            return;
+        }
+        var marker = StructuredArgument.marker("s3Request", gen -> {
+            gen.writeStartObject();
+            gen.writeStringField("method", method);
+            gen.writeStringField("bucket", bucket);
+            if (key != null) {
+                gen.writeStringField("key", key);
+            }
+            if (contentLength != null) {
+                gen.writeNumberField("contentLength", contentLength);
+            }
+            gen.writeEndObject();
+        });
+
+        if (key == null) {
+            this.requestLogger.info(marker, "S3 Client starting operation for {} {}", method, bucket);
+        } else {
+            this.requestLogger.info(marker, "S3 Client starting operation for {} {}/{}", method, bucket, key);
+        }
+    }
+
+    @Override
+    public void logResponse(String operation, String bucket, @Nullable String key, long processingTimeNanos, @Nullable Throwable exception) {
+
+    }
+}
diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientLoggerFactory.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3LoggerFactory.java
similarity index 53%
rename from experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientLoggerFactory.java
rename to experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3LoggerFactory.java
index df9a6b515..11a7d97d5 100644
--- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3ClientLoggerFactory.java
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3LoggerFactory.java
@@ -4,12 +4,14 @@
 
 import java.util.Objects;
 
-public class DefaultS3ClientLoggerFactory implements S3ClientLoggerFactory {
+public class DefaultS3LoggerFactory implements S3LoggerFactory {
+    public DefaultS3LoggerFactory() {
+    }
 
     @Override
-    public S3ClientLogger get(TelemetryConfig.LogConfig logging, Class client) {
+    public S3Logger get(TelemetryConfig.LogConfig logging, Class client) {
         if (Objects.requireNonNullElse(logging.enabled(), false)) {
-            return new DefaultS3ClientLogger(client);
+            return new DefaultS3Logger(client);
         } else {
             return null;
         }
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3Telemetry.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3Telemetry.java
new file mode 100644
index 000000000..dd14514c2
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3Telemetry.java
@@ -0,0 +1,177 @@
+package ru.tinkoff.kora.s3.client.telemetry;
+
+import jakarta.annotation.Nullable;
+
+import java.util.Collection;
+
+public final class DefaultS3Telemetry implements S3Telemetry {
+
+    private final S3Logger logger;
+    private final S3Metrics metrics;
+    private final S3Tracer tracer;
+
+    public DefaultS3Telemetry(@Nullable S3Tracer tracer,
+                              @Nullable S3Metrics metrics,
+                              @Nullable S3Logger logger) {
+        this.logger = logger;
+        this.tracer = tracer;
+        this.metrics = metrics;
+    }
+
+    public enum S3Operation {
+        PUT_OBJECT, START_MULTIPART_UPLOAD, PUT_OBJECT_PART, COMPLETE_MULTIPART_UPLOAD, ABORT_MULTIPART_UPLOAD,
+        DELETE_OBJECT, GET_OBJECT, HEAD_OBJECT, LIST_OBJECTS
+    }
+
+    private class TelemetryContext implements S3TelemetryContext {
+        private final long startNanos = System.nanoTime();
+        private final S3Operation operation;
+        private final String bucket;
+        @Nullable
+        private final String key;
+        private String awsRequestId;
+        private String awsExtendedRequestId;
+        private String uploadId;
+        private Throwable error;
+        private long contentLength = -1;
+        private int partNumber;
+
+        private TelemetryContext(S3Operation operation, String bucket, @Nullable String key) {
+            this.bucket = bucket;
+            this.key = key;
+            this.operation = operation;
+        }
+
+        @Override
+        public void setAwsRequestId(String awsRequestId) {
+            this.awsRequestId = awsRequestId;
+        }
+
+        @Override
+        public void setAwsExtendedId(String awsRequestId) {
+            this.awsExtendedRequestId = awsRequestId;
+        }
+
+        @Override
+        public void setUploadId(String awsRequestId) {
+            this.uploadId = awsRequestId;
+        }
+
+        @Override
+        public void setError(Throwable throwable) {
+            this.error = throwable;
+        }
+
+        public void setContentLength(long contentLength) {
+            this.contentLength = contentLength;
+        }
+
+        public void setPartNumber(int partNumber) {
+            this.partNumber = partNumber;
+        }
+
+        @Override
+        public void close() {
+
+        }
+    }
+
+    @Override
+    public S3TelemetryContext putObject(String bucket, String key, long contentLength) {
+        var ctx = new TelemetryContext(S3Operation.PUT_OBJECT, bucket, key);
+        ctx.setContentLength(contentLength);
+        return ctx;
+    }
+
+    @Override
+    public S3TelemetryContext startMultipartUpload(String bucket, String key) {
+        return new TelemetryContext(S3Operation.START_MULTIPART_UPLOAD, bucket, key);
+    }
+
+    @Override
+    public S3TelemetryContext putObjectPart(String bucket, String key, String uploadId, int partNumber, long contentLength) {
+        var ctx = new TelemetryContext(S3Operation.PUT_OBJECT_PART, bucket, key);
+        ctx.setUploadId(uploadId);
+        ctx.setContentLength(contentLength);
+        ctx.setPartNumber(partNumber);
+        return ctx;
+    }
+
+    @Override
+    public S3TelemetryContext completeMultipartUpload(String bucket, String key, String uploadId) {
+        var ctx = new TelemetryContext(S3Operation.COMPLETE_MULTIPART_UPLOAD, bucket, key);
+        ctx.setUploadId(uploadId);
+        return ctx;
+    }
+
+    @Override
+    public S3TelemetryContext abortMultipartUpload(String bucket, String key, String uploadId) {
+        var ctx = new TelemetryContext(S3Operation.ABORT_MULTIPART_UPLOAD, bucket, key);
+        ctx.setUploadId(uploadId);
+        return ctx;
+    }
+
+    @Override
+    public S3TelemetryContext getObject(String bucket, String key) {
+        return new TelemetryContext(S3Operation.GET_OBJECT, bucket, key);
+    }
+
+    @Override
+    public S3TelemetryContext getMetadata(String bucket, String key) {
+        return new TelemetryContext(S3Operation.HEAD_OBJECT, bucket, key);
+    }
+
+    @Override
+    public S3TelemetryContext listMetadata(String bucket, String prefix, String delimiter) {
+        return new TelemetryContext(S3Operation.LIST_OBJECTS, bucket, prefix);
+    }
+
+    @Override
+    public S3TelemetryContext deleteObject(String bucket, String key) {
+        return new TelemetryContext(S3Operation.DELETE_OBJECT, bucket, key);
+    }
+
+    @Override
+    public S3TelemetryContext deleteObjects(String bucket, Collection keys) {
+        return new TelemetryContext(S3Operation.DELETE_OBJECT, bucket, null);
+    }
+
+//    @Override
+//    public S3ClientTelemetryContext get() {
+//        var start = System.nanoTime();
+//        final S3ClientTracer.S3ClientSpan span;
+//        if (tracer != null) {
+//            span = tracer.createSpan();
+//        } else {
+//            span = null;
+//        }
+//
+//        return new S3ClientTelemetryContext() {
+//
+//            @Override
+//            public void prepared(String method, String bucket, @Nullable String key, @Nullable Long contentLength) {
+//                if (logger != null) {
+//                    logger.logRequest(method, bucket, key, contentLength);
+//                }
+//                if (span != null) {
+//                    span.prepared(method, bucket, key, contentLength);
+//                }
+//            }
+//
+//            @Override
+//            public void close(String method, String bucket, @Nullable String key, int statusCode, @Nullable S3Exception exception) {
+//                var end = System.nanoTime();
+//                var processingTime = end - start;
+//                if (metrics != null) {
+//                    metrics.record(method, bucket, key, statusCode, processingTime, exception);
+//                }
+//                if (logger != null) {
+//                    logger.logResponse(method, bucket, key, statusCode, processingTime, exception);
+//                }
+//                if (span != null) {
+//                    span.close(statusCode, exception);
+//                }
+//            }
+//        };
+//    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3TelemetryFactory.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3TelemetryFactory.java
new file mode 100644
index 000000000..e471078cd
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/DefaultS3TelemetryFactory.java
@@ -0,0 +1,41 @@
+package ru.tinkoff.kora.s3.client.telemetry;
+
+import jakarta.annotation.Nullable;
+import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
+
+public final class DefaultS3TelemetryFactory implements S3TelemetryFactory {
+
+//    private static final S3ClientTelemetry.S3ClientTelemetryContext EMPTY_CTX = new S3ClientTelemetry.S3ClientTelemetryContext() {
+//        @Override
+//        public void prepared(String method, String bucket, String key, Long contentLength) {}
+//
+//        @Override
+//        public void close(String method, String bucket, String key, int statusCode, @Nullable S3Exception exception) {}
+//    };
+//    private static final S3ClientTelemetry EMPTY_TELEMETRY = () -> EMPTY_CTX;
+
+    private final S3LoggerFactory loggerFactory;
+    private final S3TracerFactory tracingFactory;
+    private final S3MetricsFactory metricsFactory;
+
+    public DefaultS3TelemetryFactory(@Nullable S3LoggerFactory loggerFactory,
+                                     @Nullable S3TracerFactory tracingFactory,
+                                     @Nullable S3MetricsFactory metricsFactory) {
+        this.loggerFactory = loggerFactory;
+        this.tracingFactory = tracingFactory;
+        this.metricsFactory = metricsFactory;
+    }
+
+    @Override
+    public S3Telemetry get(TelemetryConfig config, Class client) {
+//        var logger = this.loggerFactory == null ? null : this.loggerFactory.get(config.logging(), client);
+//        var metrics = this.metricsFactory == null ? null : this.metricsFactory.get(config.metrics(), client);
+//        var tracer = this.tracingFactory == null ? null : this.tracingFactory.get(config.tracing(), client);
+//        if (metrics == null && tracer == null && logger == null) {
+//            return EMPTY_TELEMETRY;
+//        }
+//
+//        return new DefaultS3Telemetry(tracer, metrics, logger);
+        return null;
+    }
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Logger.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Logger.java
new file mode 100644
index 000000000..edc3d9373
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Logger.java
@@ -0,0 +1,11 @@
+package ru.tinkoff.kora.s3.client.telemetry;
+
+import jakarta.annotation.Nullable;
+
+public interface S3Logger {
+
+
+    void logRequest(String operation, String bucket, @Nullable String key, @Nullable Long contentLength);
+
+    void logResponse(String operation, String bucket, @Nullable String key, long processingTimeNanos, @Nullable Throwable exception);
+}
diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientLoggerFactory.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3LoggerFactory.java
similarity index 57%
rename from experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientLoggerFactory.java
rename to experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3LoggerFactory.java
index bce3d296c..b6c177bb6 100644
--- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientLoggerFactory.java
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3LoggerFactory.java
@@ -3,8 +3,8 @@
 import jakarta.annotation.Nullable;
 import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
 
-public interface S3ClientLoggerFactory {
+public interface S3LoggerFactory {
 
     @Nullable
-    S3ClientLogger get(TelemetryConfig.LogConfig logging, Class client);
+    S3Logger get(TelemetryConfig.LogConfig logging, Class clientImpl);
 }
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Metrics.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Metrics.java
new file mode 100644
index 000000000..386c4e007
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Metrics.java
@@ -0,0 +1,8 @@
+package ru.tinkoff.kora.s3.client.telemetry;
+
+import jakarta.annotation.Nullable;
+
+public interface S3Metrics {
+
+    void record(String operation, String bucket, long processingTimeNanos, @Nullable Throwable exception);
+}
diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTracerFactory.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3MetricsFactory.java
similarity index 56%
rename from experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTracerFactory.java
rename to experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3MetricsFactory.java
index 94aecdb4f..d09d7adee 100644
--- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientTracerFactory.java
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3MetricsFactory.java
@@ -3,8 +3,8 @@
 import jakarta.annotation.Nullable;
 import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
 
-public interface S3ClientTracerFactory {
+public interface S3MetricsFactory {
 
     @Nullable
-    S3ClientTracer get(TelemetryConfig.TracingConfig tracing, Class client);
+    S3Metrics get(TelemetryConfig.MetricsConfig metrics, Class clientImpl);
 }
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Telemetry.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Telemetry.java
new file mode 100644
index 000000000..44f6dde8f
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Telemetry.java
@@ -0,0 +1,41 @@
+package ru.tinkoff.kora.s3.client.telemetry;
+
+import jakarta.annotation.Nullable;
+
+import java.util.Collection;
+
+public interface S3Telemetry {
+
+    interface S3TelemetryContext extends AutoCloseable {
+        void setAwsRequestId(String awsRequestId);
+
+        void setAwsExtendedId(String awsRequestId);
+
+        void setUploadId(String awsRequestId);
+
+        void setError(Throwable throwable);
+
+        void close();
+    }
+
+    S3TelemetryContext getObject(String bucket, String key);
+
+    S3TelemetryContext getMetadata(String bucket, String key);
+
+    S3TelemetryContext listMetadata(String bucket, @Nullable String prefix, @Nullable String delimiter);
+
+    S3TelemetryContext deleteObject(String bucket, String key);
+
+    S3TelemetryContext deleteObjects(String bucket, Collection keys);
+
+    S3TelemetryContext putObject(String bucket, String key, long contentLength);
+
+    S3TelemetryContext startMultipartUpload(String bucket, String key);
+
+    S3TelemetryContext putObjectPart(String bucket, String key, String uploadId, int partNumber, long contentLength);
+
+    S3TelemetryContext completeMultipartUpload(String bucket, String key, String uploadId);
+
+    S3TelemetryContext abortMultipartUpload(String bucket, String key, String uploadId);
+
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3TelemetryFactory.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3TelemetryFactory.java
new file mode 100644
index 000000000..2702e3795
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3TelemetryFactory.java
@@ -0,0 +1,8 @@
+package ru.tinkoff.kora.s3.client.telemetry;
+
+import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
+
+public interface S3TelemetryFactory {
+
+    S3Telemetry get(TelemetryConfig config, Class clientImpl);
+}
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Tracer.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Tracer.java
new file mode 100644
index 000000000..df8fce5e4
--- /dev/null
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3Tracer.java
@@ -0,0 +1,14 @@
+package ru.tinkoff.kora.s3.client.telemetry;
+
+import jakarta.annotation.Nullable;
+
+public interface S3Tracer {
+
+    interface S3Span {
+        void setError(Throwable t);
+
+        void close();
+    }
+
+    S3Span createSpan(String operation, String bucket, @Nullable String key, @Nullable Long contentLength);
+}
diff --git a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientMetricsFactory.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3TracerFactory.java
similarity index 56%
rename from experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientMetricsFactory.java
rename to experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3TracerFactory.java
index 6d4df9b80..ac5532201 100644
--- a/experimental/s3-client-common/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3ClientMetricsFactory.java
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/telemetry/S3TracerFactory.java
@@ -3,8 +3,8 @@
 import jakarta.annotation.Nullable;
 import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
 
-public interface S3ClientMetricsFactory {
+public interface S3TracerFactory {
 
     @Nullable
-    S3ClientMetrics get(TelemetryConfig.MetricsConfig metrics, Class client);
+    S3Tracer get(TelemetryConfig.TracingConfig tracing, Class clientImpl);
 }
diff --git a/experimental/s3-client/src/test/java/ru/tinkoff/kora/s3/client/S3ClientTest.java b/experimental/s3-client/src/test/java/ru/tinkoff/kora/s3/client/S3ClientTest.java
new file mode 100644
index 000000000..d2b27ba52
--- /dev/null
+++ b/experimental/s3-client/src/test/java/ru/tinkoff/kora/s3/client/S3ClientTest.java
@@ -0,0 +1,570 @@
+package ru.tinkoff.kora.s3.client;
+
+import io.minio.GetObjectArgs;
+import io.minio.MakeBucketArgs;
+import io.minio.MinioClient;
+import io.minio.PutObjectArgs;
+import io.minio.errors.ErrorResponseException;
+import org.junit.jupiter.api.*;
+import org.mockito.Mockito;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.utility.DockerImageName;
+import ru.tinkoff.kora.common.util.Size;
+import ru.tinkoff.kora.http.client.common.telemetry.HttpClientTelemetry;
+import ru.tinkoff.kora.http.client.ok.OkHttpClient;
+import ru.tinkoff.kora.s3.client.exception.S3ClientErrorException;
+import ru.tinkoff.kora.s3.client.exception.S3ClientUnknownException;
+import ru.tinkoff.kora.s3.client.impl.S3ClientImpl;
+import ru.tinkoff.kora.s3.client.model.S3Body;
+import ru.tinkoff.kora.s3.client.telemetry.S3Telemetry;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class S3ClientTest {
+    static GenericContainer minio = new GenericContainer<>(DockerImageName.parse("minio/minio"))
+        .withCommand("server", "/home/shared")
+        .withEnv("SERVICES", "s3")
+        .withStartupTimeout(Duration.ofMinutes(1))
+//        .withImagePullPolicy(i -> !k8s)
+        .withNetworkAliases("s3")
+        .withExposedPorts(9000);
+    static okhttp3.OkHttpClient ok = new okhttp3.OkHttpClient.Builder()
+//        .addInterceptor(new HttpLoggingInterceptor(System.out::println).setLevel(HttpLoggingInterceptor.Level.HEADERS))
+        .build();
+    static MinioClient minioClient;
+
+    private S3Config config;
+
+    @BeforeAll
+    static void beforeAll() throws Exception {
+        minio.start();
+        minioClient = MinioClient.builder()
+            .httpClient(ok)
+            .endpoint("http://" + minio.getHost() + ":" + minio.getMappedPort(9000))
+            .credentials("minioadmin", "minioadmin")
+            .build();
+        minioClient.makeBucket(MakeBucketArgs.builder()
+            .bucket("test")
+            .build());
+    }
+
+    @AfterAll
+    static void afterAll() {
+        minio.stop();
+    }
+
+
+    @BeforeEach
+    void setUp() {
+        this.config = mock(S3Config.class);
+        when(config.endpoint()).thenReturn("http://" + minio.getHost() + ":" + minio.getMappedPort(9000));
+        when(config.addressStyle()).thenReturn(S3Config.AddressStyle.PATH);
+        when(config.region()).thenReturn("us-east-1");
+        when(config.upload()).thenReturn(Mockito.mock());
+        when(config.upload().singlePartUploadLimit()).thenCallRealMethod();
+        when(config.upload().chunkSize()).thenCallRealMethod();
+        when(config.upload().partSize()).thenCallRealMethod();
+    }
+
+    S3Client s3Client(String accessKey, String secretKey) {
+        when(config.accessKey()).thenReturn(accessKey);
+        when(config.secretKey()).thenReturn(secretKey);
+        var telemetry = mock(S3Telemetry.class);
+        when(telemetry.getObject(anyString(), anyString())).thenReturn(mock());
+        when(telemetry.getMetadata(anyString(), anyString())).thenReturn(mock());
+        when(telemetry.listMetadata(anyString(), any(), any())).thenReturn(mock());
+        when(telemetry.deleteObject(anyString(), any())).thenReturn(mock());
+        when(telemetry.deleteObjects(anyString(), any())).thenReturn(mock());
+        when(telemetry.putObject(anyString(), any(), anyLong())).thenReturn(mock());
+        when(telemetry.putObjectPart(anyString(), anyString(), anyString(), anyInt(), anyLong())).thenReturn(mock());
+        when(telemetry.startMultipartUpload(anyString(), anyString())).thenReturn(mock());
+        when(telemetry.abortMultipartUpload(anyString(), anyString(), anyString())).thenReturn(mock());
+        when(telemetry.completeMultipartUpload(anyString(), anyString(), anyString())).thenReturn(mock());
+
+        var httpTelemetry = mock(HttpClientTelemetry.class);
+
+        var httpClient = new OkHttpClient(ok);
+        return new S3ClientImpl(httpClient, config, (config, clazz) -> telemetry, (con, cl) -> httpTelemetry, Object.class);
+    }
+
+    S3Client s3Client() {
+        return s3Client("minioadmin", "minioadmin");
+    }
+
+    @Nested
+    class Get {
+        @Test
+        void testInvalidAccessKey() {
+            assertThatThrownBy(() -> s3Client("test", "test").get("test", UUID.randomUUID().toString(), null))
+                .isInstanceOf(S3ClientErrorException.class)
+                .hasFieldOrPropertyWithValue("errorCode", "InvalidAccessKeyId")
+                .hasFieldOrPropertyWithValue("errorMessage", "The Access Key Id you provided does not exist in our records.");
+        }
+
+        @Test
+        void testInvalidSecretKey() {
+            assertThatThrownBy(() -> s3Client("minioadmin", "test").get("test", UUID.randomUUID().toString(), null))
+                .isInstanceOf(S3ClientErrorException.class)
+                .hasFieldOrPropertyWithValue("errorCode", "SignatureDoesNotMatch")
+                .hasFieldOrPropertyWithValue("errorMessage", "The request signature we calculated does not match the signature you provided. Check your key and signing method.");
+        }
+
+        @Test
+        void testGetObjectThrowsErrorOnUnknownObject() {
+            assertThatThrownBy(() -> s3Client().get("test", UUID.randomUUID().toString(), null))
+                .isInstanceOf(S3ClientErrorException.class)
+                .hasFieldOrPropertyWithValue("errorCode", "NoSuchKey")
+                .hasFieldOrPropertyWithValue("errorMessage", "The specified key does not exist.");
+        }
+
+        @Test
+        void testGetObjectThrowsErrorOnUnknownBucket() {
+            assertThatThrownBy(() -> s3Client().get(UUID.randomUUID().toString(), UUID.randomUUID().toString(), null))
+                .isInstanceOf(S3ClientErrorException.class)
+                .hasFieldOrPropertyWithValue("errorCode", "NoSuchBucket")
+                .hasFieldOrPropertyWithValue("errorMessage", "The specified bucket does not exist");
+        }
+
+        @Test
+        void testGetOptionalObjectReturnsNullOnUnknownObjects() {
+            var object = s3Client().getOptional("test", UUID.randomUUID().toString(), null);
+            assertThat(object).isNull();
+        }
+
+        @Test
+        void testGetOptionalObjectReturnsNullOnUnknownBucket() {
+            var object = s3Client().getOptional(UUID.randomUUID().toString(), UUID.randomUUID().toString(), null);
+            assertThat(object).isNull();
+        }
+
+        @Test
+        void testGetValidObject() throws Exception {
+            var key = UUID.randomUUID().toString();
+            var content = UUID.randomUUID().toString().repeat(10240).getBytes(StandardCharsets.UTF_8);
+            minioClient.putObject(PutObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .contentType("text/plain")
+                .stream(new ByteArrayInputStream(content), content.length, -1)
+                .build());
+            try (var object = s3Client().get("test", key, null)) {
+                assertThat(object).isNotNull();
+                assertThat(object.meta().size()).isEqualTo(content.length);
+                assertThat(object.meta().bucket()).isEqualTo("test");
+                assertThat(object.meta().key()).isEqualTo(key);
+                try (var body = object.body()) {
+                    assertThat(body).isNotNull();
+                    assertThat(body.asBytes()).isEqualTo(content);
+                    assertThat(body.contentType()).isEqualTo("text/plain");
+                }
+            }
+        }
+
+        @Test
+        void testGetRange() throws Exception {
+            var key = UUID.randomUUID().toString();
+            var content = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
+            minioClient.putObject(PutObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .contentType("text/plain")
+                .stream(new ByteArrayInputStream(content), content.length, -1)
+                .build());
+            try (var object = s3Client().get("test", key, new S3Client.RangeData.Range(1, 5))) {
+                assertThat(object.meta().size()).isEqualTo(content.length);
+                try (var body = object.body()) {
+                    assertThat(body.size()).isEqualTo(5);
+                    assertThat(body.asBytes()).isEqualTo(Arrays.copyOfRange(content, 1, 6));
+                }
+            }
+            try (var object = s3Client().get("test", key, new S3Client.RangeData.StartFrom(5))) {
+                assertThat(object.meta().size()).isEqualTo(content.length);
+                try (var body = object.body()) {
+                    assertThat(body.size()).isEqualTo(content.length - 5);
+                    assertThat(body.asBytes()).isEqualTo(Arrays.copyOfRange(content, 5, content.length));
+                }
+            }
+            try (var object = s3Client().get("test", key, new S3Client.RangeData.LastN(5))) {
+                assertThat(object.meta().size()).isEqualTo(content.length);
+                try (var body = object.body()) {
+                    assertThat(body.size()).isEqualTo(5);
+                    assertThat(body.asBytes()).isEqualTo(Arrays.copyOfRange(content, content.length - 5, content.length));
+                }
+            }
+        }
+
+        @Test
+        void testGetOptionalValidObject() throws Exception {
+            var key = UUID.randomUUID().toString();
+            var content = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
+            minioClient.putObject(PutObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .contentType("text/plain")
+                .stream(new ByteArrayInputStream(content), content.length, -1)
+                .build());
+            try (var object = s3Client().getOptional("test", key, null)) {
+                assertThat(object).isNotNull();
+                assertThat(object.meta().size()).isEqualTo(content.length);
+                assertThat(object.meta().bucket()).isEqualTo("test");
+                assertThat(object.meta().key()).isEqualTo(key);
+                try (var body = object.body()) {
+                    assertThat(body).isNotNull();
+                    assertThat(body.asBytes()).isEqualTo(content);
+                    assertThat(body.contentType()).isEqualTo("text/plain");
+                }
+            }
+        }
+
+    }
+
+    @Nested
+    class GetMeta {
+
+        @Test
+        void testGetMetaThrowsErrorOnUnknownObject() throws Exception {
+            assertThatThrownBy(() -> s3Client().getMeta("test", UUID.randomUUID().toString()))
+                .isInstanceOf(S3ClientErrorException.class)
+                .hasFieldOrPropertyWithValue("errorCode", "NoSuchKey")
+                .hasFieldOrPropertyWithValue("errorMessage", "Object does not exist");
+        }
+
+        @Test
+        void testGetMetaThrowsErrorOnUnknownBucket() throws Exception {
+            // HEAD throws 404 without a body (because HEAD has no body), so we cannot read code and message and detect if it's no bucket or no key
+            assertThatThrownBy(() -> s3Client().getMeta(UUID.randomUUID().toString(), UUID.randomUUID().toString()))
+                .isInstanceOf(S3ClientErrorException.class)
+                .hasFieldOrPropertyWithValue("errorCode", "NoSuchKey")
+                .hasFieldOrPropertyWithValue("errorMessage", "Object does not exist");
+        }
+
+        @Test
+        void testGetMetaOptionalObjectReturnsNullOnUnknownObjects() {
+            var object = s3Client().getMetaOptional("test", UUID.randomUUID().toString());
+            assertThat(object).isNull();
+        }
+
+        @Test
+        void testGetMetaOptionalObjectReturnsNullOnUnknownBucket() {
+            var object = s3Client().getMetaOptional(UUID.randomUUID().toString(), UUID.randomUUID().toString());
+            assertThat(object).isNull();
+        }
+
+        @Test
+        void testGetMetadataValidObject() throws Exception {
+            var key = UUID.randomUUID().toString();
+            var content = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
+            minioClient.putObject(PutObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .contentType("text/plain")
+                .stream(new ByteArrayInputStream(content), content.length, -1)
+                .build());
+            var metadata = s3Client().getMeta("test", key);
+            assertThat(metadata).isNotNull();
+            assertThat(metadata.bucket()).isEqualTo("test");
+            assertThat(metadata.key()).isEqualTo(key);
+            assertThat(metadata.size()).isEqualTo(content.length);
+        }
+
+        @Test
+        void testGetOptionalMetadataValidObject() throws Exception {
+            var key = UUID.randomUUID().toString();
+            var content = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
+            minioClient.putObject(PutObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .contentType("text/plain")
+                .stream(new ByteArrayInputStream(content), content.length, -1)
+                .build());
+            var metadata = s3Client().getMetaOptional("test", key);
+            assertThat(metadata).isNotNull();
+            assertThat(metadata.bucket()).isEqualTo("test");
+            assertThat(metadata.key()).isEqualTo(key);
+            assertThat(metadata.size()).isEqualTo(content.length);
+        }
+    }
+
+    @Nested
+    class ListMeta {
+
+        @Test
+        void testListMetadata() throws Exception {
+            var prefix = "testListMetadata0" + UUID.randomUUID();
+            var key = prefix + "/" + UUID.randomUUID();
+            var content = randomBytes(1024);
+            minioClient.putObject(PutObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .contentType("text/plain")
+                .stream(new ByteArrayInputStream(content), content.length, -1)
+                .build());
+
+            assertThat(s3Client().list("test", prefix + "/", null, 10))
+                .isNotNull()
+                .hasSize(1);
+
+            for (int i = 0; i < 10; i++) {
+                var moreKey = prefix + "/" + UUID.randomUUID();
+                var moreContent = randomBytes(1024);
+                minioClient.putObject(PutObjectArgs.builder()
+                    .bucket("test")
+                    .object(moreKey)
+                    .contentType("text/plain")
+                    .stream(new ByteArrayInputStream(moreContent), moreContent.length, -1)
+                    .build());
+            }
+
+            assertThat(s3Client().list("test", prefix, null, 10))
+                .isNotNull()
+                .hasSize(10);
+            assertThat(s3Client().list("test", prefix, null, 20))
+                .isNotNull()
+                .hasSize(11);
+        }
+
+        @Test
+        void testListMetadataOnInvalidBucket() {
+            assertThatThrownBy(() -> s3Client().list(UUID.randomUUID().toString(), "testListMetadata0", null, 10))
+                .isInstanceOf(S3ClientErrorException.class)
+                .hasFieldOrPropertyWithValue("errorCode", "NoSuchBucket")
+                .hasFieldOrPropertyWithValue("errorMessage", "The specified bucket does not exist");
+        }
+
+        @Test
+        void testListMetadataIterator() throws Exception {
+            var prefix = "testListMetadataIterator" + UUID.randomUUID();
+            for (int i = 0; i < 101; i++) {
+                var key = prefix + "/" + UUID.randomUUID();
+                var content = randomBytes(1024);
+                minioClient.putObject(PutObjectArgs.builder()
+                    .bucket("test")
+                    .object(key)
+                    .contentType("text/plain")
+                    .stream(new ByteArrayInputStream(content), content.length, -1)
+                    .build());
+            }
+
+            assertThat(s3Client().listIterator("test", prefix, null, 42))
+                .toIterable()
+                .hasSize(101)
+            ;
+        }
+    }
+
+    @Nested
+    class Delete {
+
+        @Test
+        void testDeleteObjectSuccessOnValidObject() throws Exception {
+            var key = UUID.randomUUID().toString();
+            var content = randomBytes(1024);
+            minioClient.putObject(PutObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .contentType("text/plain")
+                .stream(new ByteArrayInputStream(content), content.length, -1)
+                .build());
+
+            s3Client().delete("test", key);
+
+            assertThatThrownBy(() -> minioClient.getObject(GetObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .build()))
+                .isInstanceOf(ErrorResponseException.class)
+                .extracting("errorResponse")
+                .hasFieldOrPropertyWithValue("code", "NoSuchKey");
+        }
+
+        @Test
+        void testDeleteObjectSuccessOnObjectThatDoesNotExist() throws Exception {
+            var key = UUID.randomUUID().toString();
+
+            s3Client().delete("test", key);
+        }
+
+        @Test
+        void testDeleteObjectSuccessOnBucketThatDoesNotExist() throws Exception {
+            var key = UUID.randomUUID().toString();
+
+            s3Client().delete(key, key);
+        }
+
+        @Test
+        void testDeleteObjects() throws Exception {
+            var key1 = UUID.randomUUID().toString();
+            var key2 = UUID.randomUUID().toString();
+            var key3 = UUID.randomUUID().toString();
+            var content = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
+            minioClient.putObject(PutObjectArgs.builder()
+                .bucket("test")
+                .object(key1)
+                .contentType("text/plain")
+                .stream(new ByteArrayInputStream(content), content.length, -1)
+                .build());
+            minioClient.putObject(PutObjectArgs.builder()
+                .bucket("test")
+                .object(key2)
+                .contentType("text/plain")
+                .stream(new ByteArrayInputStream(content), content.length, -1)
+                .build());
+
+            s3Client().delete("test", List.of(key1, key2, key3));
+
+            assertThatThrownBy(() -> minioClient.getObject(GetObjectArgs.builder()
+                .bucket("test")
+                .object(key1)
+                .build()))
+                .isInstanceOf(ErrorResponseException.class)
+                .extracting("errorResponse")
+                .hasFieldOrPropertyWithValue("code", "NoSuchKey");
+            assertThatThrownBy(() -> minioClient.getObject(GetObjectArgs.builder()
+                .bucket("test")
+                .object(key2)
+                .build()))
+                .isInstanceOf(ErrorResponseException.class)
+                .extracting("errorResponse")
+                .hasFieldOrPropertyWithValue("code", "NoSuchKey");
+            assertThatThrownBy(() -> minioClient.getObject(GetObjectArgs.builder()
+                .bucket("test")
+                .object(key3)
+                .build()))
+                .isInstanceOf(ErrorResponseException.class)
+                .extracting("errorResponse")
+                .hasFieldOrPropertyWithValue("code", "NoSuchKey");
+        }
+    }
+
+    @Nested
+    class Put {
+        @Test
+        void testPutByteArray() throws Exception {
+            var key = UUID.randomUUID().toString();
+            var content = randomBytes(32 * 1024);
+            s3Client().put("test", key, S3Body.ofBytes(content));
+
+            try (var rs = minioClient.getObject(GetObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .build())) {
+                assertThat(rs).hasBinaryContent(content);
+            }
+        }
+
+        @Test
+        void testByteArrayBiggerThanTwoRecommendedPartSizes() throws Exception {
+            var key = UUID.randomUUID().toString();
+            when(config.upload().singlePartUploadLimit()).thenReturn(Size.of(10, Size.Type.MiB));
+            var content = randomBytes(Size.of(21, Size.Type.MiB).toBytes());
+            s3Client().put("test", key, S3Body.ofBytes(content));
+
+            try (var rs = minioClient.getObject(GetObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .build())) {
+                assertThat(rs).hasBinaryContent(content);
+            }
+        }
+
+        @Test
+        void putKnownSizeStreamLessThanOnePart() throws Exception {
+            var key = UUID.randomUUID().toString();
+            var content = randomBytes(1024 * 1024);
+            s3Client().put("test", key, S3Body.ofInputStream(new ByteArrayInputStream(content), content.length));
+
+            try (var rs = minioClient.getObject(GetObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .build())) {
+                assertThat(rs).hasBinaryContent(content);
+            }
+        }
+
+        @Test
+        void putKnownSizeStreamThatFitsInOnePartUpload() throws Exception {
+            var key = UUID.randomUUID().toString();
+            var content = randomBytes(32 * 1023 * 1024);
+            s3Client().put("test", key, S3Body.ofInputStream(new ByteArrayInputStream(content), content.length));
+
+            try (var rs = minioClient.getObject(GetObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .build())) {
+                assertThat(rs).hasBinaryContent(content);
+            }
+        }
+
+        @Test
+        void putKnownSizeStreamThatBiggerThanTwoRecommendedPartSizes() throws Exception {
+            var key = UUID.randomUUID().toString();
+            when(config.upload().singlePartUploadLimit()).thenReturn(Size.of(10, Size.Type.MiB));
+            var content = randomBytes(Size.of(21, Size.Type.MiB).toBytes());
+            s3Client().put("test", key, S3Body.ofInputStream(new ByteArrayInputStream(content), content.length));
+
+            try (var rs = minioClient.getObject(GetObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .build())) {
+                assertThat(rs).hasBinaryContent(content);
+            }
+        }
+
+        @Test
+        void putKnownSizeStreamThatBiggerThanTwoRecommendedPartSizesWithError() throws Exception {
+            var key = UUID.randomUUID().toString();
+            when(config.upload().singlePartUploadLimit()).thenReturn(Size.of(10, Size.Type.MiB));
+            var content = randomBytes(Size.of(21, Size.Type.MiB).toBytes());
+            assertThatThrownBy(() -> s3Client().put("test", key, S3Body.ofInputStream(new ByteArrayInputStream(content), content.length + 10)))
+                .isInstanceOf(S3ClientUnknownException.class);
+
+            // todo validate that upload removed
+        }
+
+        @Test
+        void testPutUnknownSizeInputStreamThatFitsInOnePart() throws Exception {
+            var key = UUID.randomUUID().toString();
+            var content = randomBytes(6 * 1024 * 1024);
+            s3Client().put("test", key, S3Body.ofInputStream(new ByteArrayInputStream(content), -1));
+
+            try (var rs = minioClient.getObject(GetObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .build())) {
+                assertThat(rs).hasBinaryContent(content);
+            }
+        }
+
+        @Test
+        void testPutUnknownSizeLargeInputStream() throws Exception {
+            var key = UUID.randomUUID().toString();
+            var content = randomBytes(32 * 1024 * 1024);
+            s3Client().put("test", key, S3Body.ofInputStream(new ByteArrayInputStream(content), -1));
+
+            try (var rs = minioClient.getObject(GetObjectArgs.builder()
+                .bucket("test")
+                .object(key)
+                .build())) {
+                assertThat(rs).hasBinaryContent(content);
+            }
+        }
+    }
+
+    byte[] randomBytes(long len) {
+        var bytes = new byte[Math.toIntExact(len)];
+        ThreadLocalRandom.current().nextBytes(bytes);
+        return bytes;
+    }
+}
diff --git a/experimental/s3-client/src/test/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsResultSaxHandlerTest.java b/experimental/s3-client/src/test/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsResultSaxHandlerTest.java
new file mode 100644
index 000000000..47a6b0fec
--- /dev/null
+++ b/experimental/s3-client/src/test/java/ru/tinkoff/kora/s3/client/impl/xml/DeleteObjectsResultSaxHandlerTest.java
@@ -0,0 +1,41 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import org.junit.jupiter.api.Test;
+
+import javax.xml.parsers.SAXParserFactory;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DeleteObjectsResultSaxHandlerTest {
+    @Test
+    void test() throws Exception {
+        var xml = """
+            
+                
+                    b187dd1e-1fe8-4ccd-9920-63e1770a6dce
+                
+                
+                    86ed0727-c3a0-4f70-9bcb-80ba30def9bf
+                
+                
+                    c8fde66c-7ae9-46f6-91b6-43564ed01a13
+                
+            
+            """;
+        var handler = new DeleteObjectsResultSaxHandler();
+        SAXParserFactory.newDefaultInstance()
+            .newSAXParser()
+            .parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), handler);
+
+        var dto = handler.toResult();
+
+        assertThat(dto.deleted()).isEqualTo(List.of(
+            new DeleteObjectsResult.Deleted("b187dd1e-1fe8-4ccd-9920-63e1770a6dce"),
+            new DeleteObjectsResult.Deleted("86ed0727-c3a0-4f70-9bcb-80ba30def9bf"),
+            new DeleteObjectsResult.Deleted("c8fde66c-7ae9-46f6-91b6-43564ed01a13")
+        ));
+    }
+}
diff --git a/experimental/s3-client/src/test/java/ru/tinkoff/kora/s3/client/impl/xml/S3ResponseParserTest.java b/experimental/s3-client/src/test/java/ru/tinkoff/kora/s3/client/impl/xml/S3ResponseParserTest.java
new file mode 100644
index 000000000..bf3e025c1
--- /dev/null
+++ b/experimental/s3-client/src/test/java/ru/tinkoff/kora/s3/client/impl/xml/S3ResponseParserTest.java
@@ -0,0 +1,25 @@
+package ru.tinkoff.kora.s3.client.impl.xml;
+
+import org.junit.jupiter.api.Test;
+
+import javax.xml.parsers.SAXParserFactory;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class S3ResponseParserTest {
+    @Test
+    void testS3Parser() throws Exception {
+        var xml = "AccessDeniedAccess Denied.testtest/test/test18219C0F1714109Cdd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8";
+        var handler = new S3ErrorSaxHandler();
+        SAXParserFactory.newDefaultInstance()
+            .newSAXParser()
+            .parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), handler);
+
+        var dto = handler.toResult();
+
+        assertThat(dto.code()).isEqualTo("AccessDenied");
+        assertThat(dto.message()).isEqualTo("Access Denied.");
+    }
+}
diff --git a/http/http-client-async/src/main/java/ru/tinkoff/kora/http/client/async/AsyncHttpClient.java b/http/http-client-async/src/main/java/ru/tinkoff/kora/http/client/async/AsyncHttpClient.java
index 9a6d099d8..63eb33e35 100644
--- a/http/http-client-async/src/main/java/ru/tinkoff/kora/http/client/async/AsyncHttpClient.java
+++ b/http/http-client-async/src/main/java/ru/tinkoff/kora/http/client/async/AsyncHttpClient.java
@@ -22,8 +22,7 @@ public AsyncHttpClient(org.asynchttpclient.AsyncHttpClient client) {
     }
 
     @Override
-    public CompletionStage execute(HttpClientRequest request) {
-        var ctx = Context.current();
+    public CompletionStage execute(Context ctx, HttpClientRequest request) {
         return this.processRequest(ctx, request)
             .exceptionallyCompose(e -> {
                 if (e instanceof CompletionException ce) {
diff --git a/http/http-client-common/src/main/java/module-info.java b/http/http-client-common/src/main/java/module-info.java
new file mode 100644
index 000000000..65e9c3c00
--- /dev/null
+++ b/http/http-client-common/src/main/java/module-info.java
@@ -0,0 +1,16 @@
+module kora.http.client.common {
+    requires transitive kora.http.common;
+    requires transitive kora.logging.common;
+    requires transitive kora.telemetry.common;
+
+    exports ru.tinkoff.kora.http.client.common;
+    exports ru.tinkoff.kora.http.client.common.annotation;
+    exports ru.tinkoff.kora.http.client.common.auth;
+    exports ru.tinkoff.kora.http.client.common.declarative;
+    exports ru.tinkoff.kora.http.client.common.form;
+    exports ru.tinkoff.kora.http.client.common.interceptor;
+    exports ru.tinkoff.kora.http.client.common.request;
+    exports ru.tinkoff.kora.http.client.common.response;
+    exports ru.tinkoff.kora.http.client.common.telemetry;
+    exports ru.tinkoff.kora.http.client.common.writer;
+}
diff --git a/http/http-client-common/src/main/java/ru/tinkoff/kora/http/client/common/HttpClient.java b/http/http-client-common/src/main/java/ru/tinkoff/kora/http/client/common/HttpClient.java
index a493ce94e..6ef263b17 100644
--- a/http/http-client-common/src/main/java/ru/tinkoff/kora/http/client/common/HttpClient.java
+++ b/http/http-client-common/src/main/java/ru/tinkoff/kora/http/client/common/HttpClient.java
@@ -15,18 +15,22 @@
  */
 public interface HttpClient {
 
-    CompletionStage execute(HttpClientRequest request);
+    default CompletionStage execute(HttpClientRequest request) {
+        return execute(Context.current(), request);
+    }
+
+    CompletionStage execute(Context context, HttpClientRequest request);
 
     default HttpClient with(HttpClientInterceptor interceptor) {
-        return request -> {
+        return (ctx, request) -> {
             try {
-                return interceptor.processRequest(Context.current(), (context, httpClientRequest) -> {
-                    var ctx = Context.current();
+                return interceptor.processRequest(ctx, (context, httpClientRequest) -> {
+                    var old = Context.current();
                     try {
                         context.inject();
                         return this.execute(httpClientRequest);
                     } finally {
-                        ctx.inject();
+                        old.inject();
                     }
                 }, request);
             } catch (Throwable e) {
diff --git a/http/http-client-common/src/main/java/ru/tinkoff/kora/http/client/common/telemetry/DefaultHttpClientTelemetry.java b/http/http-client-common/src/main/java/ru/tinkoff/kora/http/client/common/telemetry/DefaultHttpClientTelemetry.java
index 5e4023f11..b3826cb92 100644
--- a/http/http-client-common/src/main/java/ru/tinkoff/kora/http/client/common/telemetry/DefaultHttpClientTelemetry.java
+++ b/http/http-client-common/src/main/java/ru/tinkoff/kora/http/client/common/telemetry/DefaultHttpClientTelemetry.java
@@ -190,7 +190,7 @@ private Charset detectCharset(String contentType) {
         return null;
     }
 
-    public class DefaultHttpClientTelemetryContextImpl implements HttpClientTelemetryContext {
+    class DefaultHttpClientTelemetryContextImpl implements HttpClientTelemetryContext {
         private final Context ctx;
         private final HttpClientRequest request;
         private final TelemetryContextData data;
diff --git a/http/http-client-jdk/src/main/java/ru/tinkoff/kora/http/client/jdk/JdkHttpClient.java b/http/http-client-jdk/src/main/java/ru/tinkoff/kora/http/client/jdk/JdkHttpClient.java
index 2b0920e2e..03f45aa10 100644
--- a/http/http-client-jdk/src/main/java/ru/tinkoff/kora/http/client/jdk/JdkHttpClient.java
+++ b/http/http-client-jdk/src/main/java/ru/tinkoff/kora/http/client/jdk/JdkHttpClient.java
@@ -1,5 +1,6 @@
 package ru.tinkoff.kora.http.client.jdk;
 
+import ru.tinkoff.kora.common.Context;
 import ru.tinkoff.kora.http.client.common.*;
 import ru.tinkoff.kora.http.client.common.request.HttpClientRequest;
 import ru.tinkoff.kora.http.client.common.response.HttpClientResponse;
@@ -23,7 +24,7 @@ public JdkHttpClient(java.net.http.HttpClient client) {
     }
 
     @Override
-    public CompletionStage execute(HttpClientRequest request) {
+    public CompletionStage execute(Context ctx, HttpClientRequest request) {
         var httpClientRequest = HttpRequest.newBuilder()
             .uri(request.uri());
         if (request.requestTimeout() != null) {
diff --git a/http/http-client-ok/src/main/java/ru/tinkoff/kora/http/client/ok/OkHttpClient.java b/http/http-client-ok/src/main/java/ru/tinkoff/kora/http/client/ok/OkHttpClient.java
index fc07d3785..1ba1ece5e 100644
--- a/http/http-client-ok/src/main/java/ru/tinkoff/kora/http/client/ok/OkHttpClient.java
+++ b/http/http-client-ok/src/main/java/ru/tinkoff/kora/http/client/ok/OkHttpClient.java
@@ -4,6 +4,7 @@
 import okhttp3.Request;
 import okhttp3.RequestBody;
 import okhttp3.internal.http.HttpMethod;
+import ru.tinkoff.kora.common.Context;
 import ru.tinkoff.kora.http.client.common.HttpClient;
 import ru.tinkoff.kora.http.client.common.HttpClientConnectionException;
 import ru.tinkoff.kora.http.client.common.HttpClientTimeoutException;
@@ -22,7 +23,7 @@ public OkHttpClient(okhttp3.OkHttpClient client) {
     }
 
     @Override
-    public CompletionStage execute(HttpClientRequest request) {
+    public CompletionStage execute(Context ctx, HttpClientRequest request) {
         try {
             var b = new Request.Builder();
             b.method(request.method(), toRequestBody(request))
diff --git a/http/http-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/http/client/symbol/processor/ClientClassGenerator.kt b/http/http-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/http/client/symbol/processor/ClientClassGenerator.kt
index 66f684c63..d27d7c082 100644
--- a/http/http-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/http/client/symbol/processor/ClientClassGenerator.kt
+++ b/http/http-client-symbol-processor/src/main/kotlin/ru/tinkoff/kora/http/client/symbol/processor/ClientClassGenerator.kt
@@ -154,7 +154,7 @@ class ClientClassGenerator(private val resolver: Resolver) {
 
     private fun buildFunction(methodData: MethodData): FunSpec {
         val method = methodData.declaration
-        val m = method.overridingKeepAop(resolver)
+        val m = method.overridingKeepAop()
         val b = CodeBlock.builder()
 
         val httpRoute = method.findAnnotation(HttpClientClassNames.httpRoute)!!
diff --git a/http/http-common/src/main/java/module-info.java b/http/http-common/src/main/java/module-info.java
new file mode 100644
index 000000000..2090d1c47
--- /dev/null
+++ b/http/http-common/src/main/java/module-info.java
@@ -0,0 +1,13 @@
+module kora.http.common {
+    requires transitive kora.common;
+    requires transitive org.slf4j;
+    requires transitive java.net.http;
+
+    exports ru.tinkoff.kora.http.common;
+    exports ru.tinkoff.kora.http.common.annotation;
+    exports ru.tinkoff.kora.http.common.auth;
+    exports ru.tinkoff.kora.http.common.body;
+    exports ru.tinkoff.kora.http.common.cookie;
+    exports ru.tinkoff.kora.http.common.form;
+    exports ru.tinkoff.kora.http.common.header;
+}
diff --git a/internal/test-kafka/src/main/java/ru/tinkoff/kora/test/kafka/KafkaTestContainer.java b/internal/test-kafka/src/main/java/ru/tinkoff/kora/test/kafka/KafkaTestContainer.java
index 9c5f58811..3619d7895 100644
--- a/internal/test-kafka/src/main/java/ru/tinkoff/kora/test/kafka/KafkaTestContainer.java
+++ b/internal/test-kafka/src/main/java/ru/tinkoff/kora/test/kafka/KafkaTestContainer.java
@@ -35,7 +35,7 @@ private static synchronized void init() throws Exception {
                 return;
             }
             if (container == null) {
-                container = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:5.4.3"))
+                container = new KafkaContainer(DockerImageName.parse("apache/kafka-native:3.8.0"))
                     .withExposedPorts(9092, 9093)
                     .waitingFor(Wait.forListeningPort())
                 ;
diff --git a/json/json-common/src/main/java/module-info.java b/json/json-common/src/main/java/module-info.java
new file mode 100644
index 000000000..148f3f2ed
--- /dev/null
+++ b/json/json-common/src/main/java/module-info.java
@@ -0,0 +1,9 @@
+module kora.json.common {
+    requires transitive kora.common;
+    requires transitive com.fasterxml.jackson.core;
+    requires static kotlin.stdlib;
+
+    exports ru.tinkoff.kora.json.common;
+    exports ru.tinkoff.kora.json.common.annotation;
+    exports ru.tinkoff.kora.json.common.util;
+}
diff --git a/kafka/kafka-symbol-processor/src/main/kotlin/ru/tinkoff/kora/kafka/symbol/processor/producer/KafkaPublisherGenerator.kt b/kafka/kafka-symbol-processor/src/main/kotlin/ru/tinkoff/kora/kafka/symbol/processor/producer/KafkaPublisherGenerator.kt
index 5aeae836f..14f3ff354 100644
--- a/kafka/kafka-symbol-processor/src/main/kotlin/ru/tinkoff/kora/kafka/symbol/processor/producer/KafkaPublisherGenerator.kt
+++ b/kafka/kafka-symbol-processor/src/main/kotlin/ru/tinkoff/kora/kafka/symbol/processor/producer/KafkaPublisherGenerator.kt
@@ -310,7 +310,7 @@ class KafkaPublisherGenerator(val env: SymbolProcessorEnvironment, val resolver:
     private val resumeWithException = MemberName("kotlin.coroutines", "resumeWithException")
 
     private fun generatePublisherExecutableMethod(publishMethod: KSFunctionDeclaration, publishData: KafkaPublisherUtils.PublisherData, topicVariable: String, keyParserName: String?, valueParserName: String): FunSpec {
-        val b = publishMethod.overridingKeepAop(resolver)
+        val b = publishMethod.overridingKeepAop()
         if (publishData.recordVar != null) {
             val record = publishData.recordVar.name?.asString().toString()
             b.addStatement("val _headers = %N.headers()", record)
diff --git a/logging/logging-common/src/main/java/module-info.java b/logging/logging-common/src/main/java/module-info.java
new file mode 100644
index 000000000..76823667b
--- /dev/null
+++ b/logging/logging-common/src/main/java/module-info.java
@@ -0,0 +1,10 @@
+module kora.logging.common {
+    requires transitive kora.config.common;
+    requires transitive kora.json.common;
+    requires transitive org.slf4j;
+    requires transitive static jul.to.slf4j;
+
+    exports ru.tinkoff.kora.logging.common;
+    exports ru.tinkoff.kora.logging.common.annotation;
+    exports ru.tinkoff.kora.logging.common.arg;
+}
diff --git a/micrometer/micrometer-module/build.gradle b/micrometer/micrometer-module/build.gradle
index e95cbb6fe..698f50332 100644
--- a/micrometer/micrometer-module/build.gradle
+++ b/micrometer/micrometer-module/build.gradle
@@ -20,7 +20,7 @@ dependencies {
     compileOnly project(':resilient:resilient-kora')
     compileOnly project(':cache:cache-common')
     compileOnly project(':cache:cache-caffeine')
-    compileOnly project(':experimental:s3-client-common')
+    compileOnly project(':experimental:s3-client')
     compileOnly project(':experimental:camunda-engine-bpmn')
     compileOnly project(':experimental:camunda-rest-undertow')
     compileOnly project(':experimental:camunda-zeebe-worker')
diff --git a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/MetricsModule.java b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/MetricsModule.java
index 9af7caf18..983a43bf5 100644
--- a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/MetricsModule.java
+++ b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/MetricsModule.java
@@ -32,7 +32,7 @@
 import ru.tinkoff.kora.micrometer.module.resilient.MicrometerRetryMetrics;
 import ru.tinkoff.kora.micrometer.module.resilient.MicrometerTimeoutMetrics;
 import ru.tinkoff.kora.micrometer.module.s3.client.MicrometerS3ClientMetricsFactory;
-import ru.tinkoff.kora.micrometer.module.s3.client.MicrometerS3KoraClientMetricsFactory;
+import ru.tinkoff.kora.micrometer.module.s3.client.MicrometerS3MetricsFactory;
 import ru.tinkoff.kora.micrometer.module.scheduling.MicrometerSchedulingMetricsFactory;
 import ru.tinkoff.kora.micrometer.module.soap.client.MicrometerSoapClientMetricsFactory;
 
@@ -148,8 +148,8 @@ default MicrometerS3ClientMetricsFactory micrometerS3ClientMetricsFactory(MeterR
     }
 
     @DefaultComponent
-    default MicrometerS3KoraClientMetricsFactory micrometerS3KoraClientMetricsFactory(MeterRegistry meterRegistry, MetricsConfig metricsConfig) {
-        return new MicrometerS3KoraClientMetricsFactory(meterRegistry, metricsConfig);
+    default MicrometerS3MetricsFactory micrometerS3KoraClientMetricsFactory(MeterRegistry meterRegistry, MetricsConfig metricsConfig) {
+        return new MicrometerS3MetricsFactory(meterRegistry, metricsConfig);
     }
 
     @DefaultComponent
diff --git a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/MicrometerS3ClientMetricsFactory.java b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/MicrometerS3ClientMetricsFactory.java
index 17c1efb46..fd431bdd7 100644
--- a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/MicrometerS3ClientMetricsFactory.java
+++ b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/MicrometerS3ClientMetricsFactory.java
@@ -3,8 +3,6 @@
 import io.micrometer.core.instrument.MeterRegistry;
 import jakarta.annotation.Nullable;
 import ru.tinkoff.kora.micrometer.module.MetricsConfig;
-import ru.tinkoff.kora.s3.client.telemetry.S3ClientMetrics;
-import ru.tinkoff.kora.s3.client.telemetry.S3ClientMetricsFactory;
 import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
 
 import java.util.Objects;
diff --git a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/MicrometerS3KoraClientMetricsFactory.java b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/MicrometerS3MetricsFactory.java
similarity index 59%
rename from micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/MicrometerS3KoraClientMetricsFactory.java
rename to micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/MicrometerS3MetricsFactory.java
index f008ba28c..400e13f46 100644
--- a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/MicrometerS3KoraClientMetricsFactory.java
+++ b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/MicrometerS3MetricsFactory.java
@@ -3,18 +3,16 @@
 import io.micrometer.core.instrument.MeterRegistry;
 import jakarta.annotation.Nullable;
 import ru.tinkoff.kora.micrometer.module.MetricsConfig;
-import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientMetrics;
-import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientMetricsFactory;
 import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
 
 import java.util.Objects;
 
-public class MicrometerS3KoraClientMetricsFactory implements S3KoraClientMetricsFactory {
+public class MicrometerS3MetricsFactory implements S3KoraClientMetricsFactory {
 
     private final MetricsConfig metricsConfig;
     private final MeterRegistry meterRegistry;
 
-    public MicrometerS3KoraClientMetricsFactory(MeterRegistry meterRegistry, MetricsConfig metricsConfig) {
+    public MicrometerS3MetricsFactory(MeterRegistry meterRegistry, MetricsConfig metricsConfig) {
         this.metricsConfig = metricsConfig;
         this.meterRegistry = meterRegistry;
     }
@@ -24,8 +22,8 @@ public MicrometerS3KoraClientMetricsFactory(MeterRegistry meterRegistry, Metrics
     public S3KoraClientMetrics get(TelemetryConfig.MetricsConfig config, Class client) {
         if (Objects.requireNonNullElse(config.enabled(), true)) {
             return switch (this.metricsConfig.opentelemetrySpec()) {
-                case V120 -> new Opentelemetry120S3KoraClientMetrics(this.meterRegistry, config, client);
-                case V123 -> new Opentelemetry123S3KoraClientMetrics(this.meterRegistry, config, client);
+                case V120 -> new Opentelemetry120S3Metrics(this.meterRegistry, config, client);
+                case V123 -> new Opentelemetry123S3Metrics(this.meterRegistry, config, client);
             };
         } else {
             return null;
diff --git a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry120S3ClientMetrics.java b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry120S3ClientMetrics.java
index 35a503eab..76c291aa5 100644
--- a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry120S3ClientMetrics.java
+++ b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry120S3ClientMetrics.java
@@ -5,8 +5,6 @@
 import io.opentelemetry.api.common.AttributeKey;
 import io.opentelemetry.semconv.incubating.AwsIncubatingAttributes;
 import jakarta.annotation.Nullable;
-import ru.tinkoff.kora.s3.client.S3Exception;
-import ru.tinkoff.kora.s3.client.telemetry.S3ClientMetrics;
 import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
 
 import java.util.Objects;
diff --git a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry120S3KoraClientMetrics.java b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry120S3Metrics.java
similarity index 87%
rename from micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry120S3KoraClientMetrics.java
rename to micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry120S3Metrics.java
index 1aba81cdc..2d4a222c9 100644
--- a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry120S3KoraClientMetrics.java
+++ b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry120S3Metrics.java
@@ -5,8 +5,6 @@
 import io.opentelemetry.api.common.AttributeKey;
 import io.opentelemetry.semconv.incubating.AwsIncubatingAttributes;
 import jakarta.annotation.Nullable;
-import ru.tinkoff.kora.s3.client.S3Exception;
-import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientMetrics;
 import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
 
 import java.util.Objects;
@@ -14,7 +12,7 @@
 
 import static io.opentelemetry.api.common.AttributeKey.stringKey;
 
-public class Opentelemetry120S3KoraClientMetrics implements S3KoraClientMetrics {
+public class Opentelemetry120S3Metrics implements S3KoraClientMetrics {
 
     private static final AttributeKey ERROR_CODE = stringKey("aws.error.code");
     private static final AttributeKey CLIENT_NAME = stringKey("aws.client.name");
@@ -24,7 +22,7 @@ public class Opentelemetry120S3KoraClientMetrics implements S3KoraClientMetrics
     private final TelemetryConfig.MetricsConfig config;
     private final Class client;
 
-    public Opentelemetry120S3KoraClientMetrics(MeterRegistry meterRegistry, TelemetryConfig.MetricsConfig config, Class client) {
+    public Opentelemetry120S3Metrics(MeterRegistry meterRegistry, TelemetryConfig.MetricsConfig config, Class client) {
         this.meterRegistry = meterRegistry;
         this.config = config;
         this.client = client;
diff --git a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry123S3ClientMetrics.java b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry123S3ClientMetrics.java
index 1da683d09..74689e15a 100644
--- a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry123S3ClientMetrics.java
+++ b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry123S3ClientMetrics.java
@@ -6,8 +6,6 @@
 import io.opentelemetry.semconv.HttpAttributes;
 import io.opentelemetry.semconv.incubating.AwsIncubatingAttributes;
 import jakarta.annotation.Nullable;
-import ru.tinkoff.kora.s3.client.S3Exception;
-import ru.tinkoff.kora.s3.client.telemetry.S3ClientMetrics;
 import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
 
 import java.util.Objects;
diff --git a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry123S3KoraClientMetrics.java b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry123S3Metrics.java
similarity index 87%
rename from micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry123S3KoraClientMetrics.java
rename to micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry123S3Metrics.java
index b5a91c9cd..55c7f6108 100644
--- a/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry123S3KoraClientMetrics.java
+++ b/micrometer/micrometer-module/src/main/java/ru/tinkoff/kora/micrometer/module/s3/client/Opentelemetry123S3Metrics.java
@@ -5,8 +5,6 @@
 import io.opentelemetry.api.common.AttributeKey;
 import io.opentelemetry.semconv.incubating.AwsIncubatingAttributes;
 import jakarta.annotation.Nullable;
-import ru.tinkoff.kora.s3.client.S3Exception;
-import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientMetrics;
 import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
 
 import java.util.Objects;
@@ -14,7 +12,7 @@
 
 import static io.opentelemetry.api.common.AttributeKey.stringKey;
 
-public class Opentelemetry123S3KoraClientMetrics implements S3KoraClientMetrics {
+public class Opentelemetry123S3Metrics implements S3KoraClientMetrics {
 
     private static final AttributeKey ERROR_CODE = stringKey("aws.error.code");
     private static final AttributeKey CLIENT_NAME = stringKey("aws.client.name");
@@ -24,7 +22,7 @@ public class Opentelemetry123S3KoraClientMetrics implements S3KoraClientMetrics
     private final TelemetryConfig.MetricsConfig config;
     private final Class client;
 
-    public Opentelemetry123S3KoraClientMetrics(MeterRegistry meterRegistry, TelemetryConfig.MetricsConfig config, Class client) {
+    public Opentelemetry123S3Metrics(MeterRegistry meterRegistry, TelemetryConfig.MetricsConfig config, Class client) {
         this.meterRegistry = meterRegistry;
         this.config = config;
         this.client = client;
diff --git a/opentelemetry/opentelemetry-module/build.gradle b/opentelemetry/opentelemetry-module/build.gradle
index 6f8dccf4a..7454bb66d 100644
--- a/opentelemetry/opentelemetry-module/build.gradle
+++ b/opentelemetry/opentelemetry-module/build.gradle
@@ -11,7 +11,7 @@ dependencies {
     compileOnly project(':database:database-common')
     compileOnly project(':scheduling:scheduling-common')
     compileOnly project(':cache:cache-common')
-    compileOnly project(':experimental:s3-client-common')
+    compileOnly project(':experimental:s3-client')
     compileOnly project(':experimental:camunda-rest-undertow')
     compileOnly project(':experimental:camunda-engine-bpmn')
     compileOnly project(':experimental:camunda-zeebe-worker')
diff --git a/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/OpentelemetryModule.java b/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/OpentelemetryModule.java
index 118e45921..2a09c2f38 100644
--- a/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/OpentelemetryModule.java
+++ b/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/OpentelemetryModule.java
@@ -15,7 +15,7 @@
 import ru.tinkoff.kora.opentelemetry.module.kafka.consumer.OpentelemetryKafkaConsumerTracerFactory;
 import ru.tinkoff.kora.opentelemetry.module.kafka.consumer.OpentelemetryKafkaProducerTracerFactory;
 import ru.tinkoff.kora.opentelemetry.module.s3.client.OpentelemetryS3ClientTracerFactory;
-import ru.tinkoff.kora.opentelemetry.module.s3.client.OpentelemetryS3KoraClientTracerFactory;
+import ru.tinkoff.kora.opentelemetry.module.s3.client.OpentelemetryS3TracerFactory;
 import ru.tinkoff.kora.opentelemetry.module.scheduling.OpentelemetrySchedulingTracerFactory;
 import ru.tinkoff.kora.opentelemetry.module.soap.client.OpentelemetrySoapClientTracerFactory;
 
@@ -82,8 +82,8 @@ default OpentelemetryS3ClientTracerFactory opentelemetryS3ClientTracerFactory(Tr
     }
 
     @DefaultComponent
-    default OpentelemetryS3KoraClientTracerFactory opentelemetryS3KoraClientTracerFactory(Tracer tracer) {
-        return new OpentelemetryS3KoraClientTracerFactory(tracer);
+    default OpentelemetryS3TracerFactory opentelemetryS3KoraClientTracerFactory(Tracer tracer) {
+        return new OpentelemetryS3TracerFactory(tracer);
     }
 
     @DefaultComponent
diff --git a/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3ClientTracer.java b/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3ClientTracer.java
index 9bdcf56be..f5b289b53 100644
--- a/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3ClientTracer.java
+++ b/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3ClientTracer.java
@@ -9,8 +9,6 @@
 import io.opentelemetry.semconv.incubating.HttpIncubatingAttributes;
 import jakarta.annotation.Nullable;
 import ru.tinkoff.kora.opentelemetry.common.OpentelemetryContext;
-import ru.tinkoff.kora.s3.client.S3Exception;
-import ru.tinkoff.kora.s3.client.telemetry.S3ClientTracer;
 
 import static io.opentelemetry.api.common.AttributeKey.stringKey;
 
diff --git a/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3ClientTracerFactory.java b/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3ClientTracerFactory.java
index 441783e08..81d938f3e 100644
--- a/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3ClientTracerFactory.java
+++ b/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3ClientTracerFactory.java
@@ -2,8 +2,6 @@
 
 import io.opentelemetry.api.trace.Tracer;
 import jakarta.annotation.Nullable;
-import ru.tinkoff.kora.s3.client.telemetry.S3ClientTracer;
-import ru.tinkoff.kora.s3.client.telemetry.S3ClientTracerFactory;
 import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
 
 import java.util.Objects;
diff --git a/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3KoraClientTracer.java b/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3Tracer.java
similarity index 61%
rename from opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3KoraClientTracer.java
rename to opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3Tracer.java
index 3e5e8e327..9a74a8c43 100644
--- a/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3KoraClientTracer.java
+++ b/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3Tracer.java
@@ -6,31 +6,29 @@
 import io.opentelemetry.api.trace.Tracer;
 import io.opentelemetry.semconv.incubating.AwsIncubatingAttributes;
 import io.opentelemetry.semconv.incubating.HttpIncubatingAttributes;
+import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes;
 import jakarta.annotation.Nullable;
 import ru.tinkoff.kora.opentelemetry.common.OpentelemetryContext;
-import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientTracer;
+import ru.tinkoff.kora.s3.client.exception.S3ClientErrorException;
+import ru.tinkoff.kora.s3.client.telemetry.S3Tracer;
 
 import static io.opentelemetry.api.common.AttributeKey.stringKey;
 
-public final class OpentelemetryS3KoraClientTracer implements S3KoraClientTracer {
-
-    private static final AttributeKey CLIENT_NAME = stringKey("client.name");
+//https://opentelemetry.io/docs/specs/semconv/object-stores/s3/
+public final class OpentelemetryS3Tracer implements S3Tracer {
+    private static final AttributeKey CLIENT_NAME = stringKey("aws.client.name");
     private static final AttributeKey ERROR_CODE = stringKey("aws.error.code");
-    private static final AttributeKey OPERATION_NAME = stringKey("aws.operation.name");
 
     private final Class client;
     private final Tracer tracer;
 
-    public OpentelemetryS3KoraClientTracer(Class client, Tracer tracer) {
+    public OpentelemetryS3Tracer(Class client, Tracer tracer) {
         this.client = client;
         this.tracer = tracer;
     }
 
     @Override
-    public S3KoraClientSpan createSpan(String operation,
-                                       String bucket,
-                                       @Nullable String key,
-                                       @Nullable Long contentLength) {
+    public S3Tracer.S3Span createSpan(String operation, String bucket, @Nullable String key, @Nullable Long contentLength) {
         var ctx = ru.tinkoff.kora.common.Context.current();
         var tctx = OpentelemetryContext.get(ctx);
 
@@ -39,8 +37,10 @@ public S3KoraClientSpan createSpan(String operation,
             .setParent(tctx.getContext())
             .startSpan();
 
-        span.setAttribute(CLIENT_NAME, client.getSimpleName());
-        span.setAttribute(OPERATION_NAME, operation);
+        span.setAttribute(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api");
+        span.setAttribute(RpcIncubatingAttributes.RPC_SERVICE, "s3");
+        span.setAttribute(RpcIncubatingAttributes.RPC_METHOD, operation);
+        span.setAttribute(OpentelemetryS3Tracer.CLIENT_NAME, client.getSimpleName());
         span.setAttribute(AwsIncubatingAttributes.AWS_S3_BUCKET, bucket);
         if (key != null) {
             span.setAttribute(AwsIncubatingAttributes.AWS_S3_KEY, key);
@@ -51,10 +51,13 @@ public S3KoraClientSpan createSpan(String operation,
 
         return exception -> {
             if (exception != null) {
-                span.setAttribute(ERROR_CODE.getKey(), exception.getErrorCode());
                 span.setStatus(StatusCode.ERROR);
                 span.recordException(exception);
+                if (exception instanceof S3ClientErrorException error) {
+                    span.setAttribute(ERROR_CODE.getKey(), error.getErrorCode());
+                }
             }
+            span.setAttribute(AwsIncubatingAttributes.AWS_REQUEST_ID, key);
             span.end();
         };
     }
diff --git a/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3KoraClientTracerFactory.java b/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3TracerFactory.java
similarity index 59%
rename from opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3KoraClientTracerFactory.java
rename to opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3TracerFactory.java
index 6773dabb3..3db241d54 100644
--- a/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3KoraClientTracerFactory.java
+++ b/opentelemetry/opentelemetry-module/src/main/java/ru/tinkoff/kora/opentelemetry/module/s3/client/OpentelemetryS3TracerFactory.java
@@ -2,17 +2,15 @@
 
 import io.opentelemetry.api.trace.Tracer;
 import jakarta.annotation.Nullable;
-import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientTracer;
-import ru.tinkoff.kora.s3.client.telemetry.S3KoraClientTracerFactory;
 import ru.tinkoff.kora.telemetry.common.TelemetryConfig;
 
 import java.util.Objects;
 
-public final class OpentelemetryS3KoraClientTracerFactory implements S3KoraClientTracerFactory {
+public final class OpentelemetryS3TracerFactory implements S3KoraClientTracerFactory {
 
     private final Tracer tracer;
 
-    public OpentelemetryS3KoraClientTracerFactory(Tracer tracer) {
+    public OpentelemetryS3TracerFactory(Tracer tracer) {
         this.tracer = tracer;
     }
 
@@ -20,7 +18,7 @@ public OpentelemetryS3KoraClientTracerFactory(Tracer tracer) {
     @Override
     public S3KoraClientTracer get(TelemetryConfig.TracingConfig config, Class client) {
         if (Objects.requireNonNullElse(config.enabled(), true)) {
-            return new OpentelemetryS3KoraClientTracer(client, tracer);
+            return new OpentelemetryS3Tracer(client, tracer);
         } else {
             return null;
         }
diff --git a/settings.gradle b/settings.gradle
index 18f037917..6040ef7c3 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -106,9 +106,7 @@ include(
     'mapstruct:mapstruct-ksp-extension',
     'experimental:s3-client-annotation-processor',
     'experimental:s3-client-symbol-processor',
-    'experimental:s3-client-common',
-    'experimental:s3-client-minio',
-    'experimental:s3-client-aws',
+    'experimental:s3-client',
     'experimental:camunda-engine-bpmn',
     'experimental:camunda-rest-undertow',
     'experimental:camunda-zeebe-worker',
diff --git a/symbol-processor-common/src/main/kotlin/ru/tinkoff/kora/ksp/common/AnnotationUtils.kt b/symbol-processor-common/src/main/kotlin/ru/tinkoff/kora/ksp/common/AnnotationUtils.kt
index 28548399a..364fb6dea 100644
--- a/symbol-processor-common/src/main/kotlin/ru/tinkoff/kora/ksp/common/AnnotationUtils.kt
+++ b/symbol-processor-common/src/main/kotlin/ru/tinkoff/kora/ksp/common/AnnotationUtils.kt
@@ -66,6 +66,7 @@ object AnnotationUtils {
             .filter { it.name!!.asString() == name }
             .filter { !defaultValues.contains(it) }
             .map { it.value!! }
+            .filter { it is T }
             .map { it as T }
             .firstOrNull()
     }
diff --git a/symbol-processor-common/src/main/kotlin/ru/tinkoff/kora/ksp/common/CommonAopUtils.kt b/symbol-processor-common/src/main/kotlin/ru/tinkoff/kora/ksp/common/CommonAopUtils.kt
index 308f1ff4a..6ef174fe9 100644
--- a/symbol-processor-common/src/main/kotlin/ru/tinkoff/kora/ksp/common/CommonAopUtils.kt
+++ b/symbol-processor-common/src/main/kotlin/ru/tinkoff/kora/ksp/common/CommonAopUtils.kt
@@ -2,14 +2,9 @@ package ru.tinkoff.kora.ksp.common
 
 import com.google.devtools.ksp.isProtected
 import com.google.devtools.ksp.isPublic
-import com.google.devtools.ksp.processing.Resolver
 import com.google.devtools.ksp.symbol.*
 import com.squareup.kotlinpoet.*
-import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
-import com.squareup.kotlinpoet.ksp.toAnnotationSpec
-import com.squareup.kotlinpoet.ksp.toClassName
-import com.squareup.kotlinpoet.ksp.toTypeName
-import com.squareup.kotlinpoet.ksp.toTypeVariableName
+import com.squareup.kotlinpoet.ksp.*
 import ru.tinkoff.kora.ksp.common.AnnotationUtils.isAnnotationPresent
 import ru.tinkoff.kora.ksp.common.CommonClassNames.aopAnnotation
 import ru.tinkoff.kora.ksp.common.KspCommonUtils.resolveToUnderlying
@@ -39,7 +34,7 @@ object CommonAopUtils {
         return b
     }
 
-    fun KSFunctionDeclaration.overridingKeepAop(resolver: Resolver): FunSpec.Builder {
+    fun KSFunctionDeclaration.overridingKeepAop(): FunSpec.Builder {
         val funDeclaration = this
         val funBuilder = FunSpec.builder(funDeclaration.simpleName.asString())
         if (funDeclaration.modifiers.contains(Modifier.SUSPEND)) {
@@ -61,9 +56,9 @@ object CommonAopUtils {
                 funBuilder.addAnnotation(annotation.toAnnotationSpec())
             }
         }
-        val returnType = funDeclaration.returnType!!.resolve()
-        if (returnType != resolver.builtIns.unitType) {
-            funBuilder.returns(returnType.toTypeName())
+        val returnType = funDeclaration.returnType!!.resolve().toTypeName()
+        if (returnType != UNIT) {
+            funBuilder.returns(returnType)
         }
         for (parameter in funDeclaration.parameters) {
             val parameterType = parameter.type
diff --git a/symbol-processor-common/src/testFixtures/kotlin/ru/tinkoff/kora/ksp/common/AbstractSymbolProcessorTest.kt b/symbol-processor-common/src/testFixtures/kotlin/ru/tinkoff/kora/ksp/common/AbstractSymbolProcessorTest.kt
index c7d861054..1807e408d 100644
--- a/symbol-processor-common/src/testFixtures/kotlin/ru/tinkoff/kora/ksp/common/AbstractSymbolProcessorTest.kt
+++ b/symbol-processor-common/src/testFixtures/kotlin/ru/tinkoff/kora/ksp/common/AbstractSymbolProcessorTest.kt
@@ -259,7 +259,14 @@ abstract class AbstractSymbolProcessorTest {
 
     protected fun newObject(name: String, vararg args: Any?): TestObject {
         val loadClass = compileResult.loadClass(name)
-        val inst = loadClass.constructors[0].newInstance(*args)!!
+        val mappedArgs = args.map {
+            if (it is GeneratedObject<*>) {
+                it()
+            } else {
+                it
+            }
+        }.toTypedArray()
+        val inst = loadClass.constructors[0].newInstance(*mappedArgs)!!
         return TestObject(loadClass.kotlin, inst)
     }
 
@@ -269,7 +276,7 @@ abstract class AbstractSymbolProcessorTest {
     ) {
 
         @SuppressWarnings("unchecked")
-        fun  invoke(method: String, vararg args: Any?): T? {
+        fun  invoke(method: String, vararg args: Any?): T {
             for (repositoryClassMethod in objectClass.memberFunctions) {
                 if (repositoryClassMethod.name == method && repositoryClassMethod.parameters.size == args.size + 1) {
                     try {
@@ -290,7 +297,7 @@ abstract class AbstractSymbolProcessorTest {
                             is Mono<*> -> result.block()
                             is Future<*> -> result.get()
                             else -> result
-                        } as T?
+                        } as T
                     } catch (e: InvocationTargetException) {
                         throw e.targetException
                     }
diff --git a/telemetry/telemetry-common/src/main/java/module-info.java b/telemetry/telemetry-common/src/main/java/module-info.java
new file mode 100644
index 000000000..d062946d9
--- /dev/null
+++ b/telemetry/telemetry-common/src/main/java/module-info.java
@@ -0,0 +1,5 @@
+module kora.telemetry.common {
+    requires transitive kora.config.common;
+
+    exports ru.tinkoff.kora.telemetry.common;
+}

From 76af995b4526127392fd54781158d514047184b0 Mon Sep 17 00:00:00 2001
From: Anton Duyun 
Date: Thu, 9 Oct 2025 13:14:34 +0300
Subject: [PATCH 2/2] tmp

---
 .../kora/s3/client/exception/S3ClientErrorException.java     | 2 +-
 .../java/ru/tinkoff/kora/s3/client/impl/S3ClientImpl.java    | 2 +-
 .../kora/s3/client/impl/xml/ListBucketResultSaxHandler.java  | 5 +----
 .../java/ru/tinkoff/kora/s3/client/impl/xml/S3Error.java     | 2 +-
 .../tinkoff/kora/s3/client/impl/xml/S3ErrorSaxHandler.java   | 2 +-
 5 files changed, 5 insertions(+), 8 deletions(-)

diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientErrorException.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientErrorException.java
index e54d161da..792721be1 100644
--- a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientErrorException.java
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/exception/S3ClientErrorException.java
@@ -13,7 +13,7 @@ public S3ClientErrorException(int httpCode, String errorCode, String errorMessag
     }
 
     public S3ClientErrorException(Throwable cause, int httpCode, String errorCode, String errorMessage, String requestId) {
-        super("S3ClientError: httpCode=%d, requestId=%s, errorCode=%s, errorMessage=%s".formatted(httpCode, requestId, errorMessage, errorMessage), cause, httpCode);
+        super("S3ClientError: httpCode=%d, requestId=%s, errorCode=%s, errorMessage=%s".formatted(httpCode, requestId, errorCode, errorMessage), cause, httpCode);
         this.errorCode = errorCode;
         this.errorMessage = errorMessage;
         this.requestId = requestId;
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3ClientImpl.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3ClientImpl.java
index 0e8cbe383..79e51351f 100644
--- a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3ClientImpl.java
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/S3ClientImpl.java
@@ -383,7 +383,7 @@ static RuntimeException parseS3Exception(HttpClientResponse rs, HttpBodyInput bo
             var bytes = is.readAllBytes();
             try {
                 var s3Error = S3Error.fromXml(new ByteArrayInputStream(bytes));
-                throw new S3ClientErrorException(rs.code(), s3Error.code(), s3Error.message(), s3Error.requestId());
+                throw new S3ClientErrorException(rs.code(), s3Error.code(), Objects.requireNonNullElse(s3Error.message(), ""), s3Error.requestId());
             } catch (S3ClientException e) {
                 throw e;
             } catch (Exception e) {
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/ListBucketResultSaxHandler.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/ListBucketResultSaxHandler.java
index 1c7e1533e..2377c0eae 100644
--- a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/ListBucketResultSaxHandler.java
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/ListBucketResultSaxHandler.java
@@ -17,7 +17,7 @@ public class ListBucketResultSaxHandler extends DefaultHandler {
     private int maxKeys;
     private String delimiter;
     private boolean isTruncated;
-    private List contents;
+    private List contents = new ArrayList<>();
     private String nextContinuationToken;
 
     private DefaultHandler delegate = null;
@@ -72,9 +72,6 @@ public void endElement(String uri, String localName, String qName) throws SAXExc
                     break;
                 case "Contents":
                     assert delegate instanceof ListBucketResultContentSaxHandler;
-                    if (contents == null) {
-                        contents = new ArrayList<>();
-                    }
                     contents.add(((ListBucketResultContentSaxHandler) delegate).toResult());
                     delegate = null;
                     break;
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3Error.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3Error.java
index 3d0e20251..909688dff 100644
--- a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3Error.java
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3Error.java
@@ -10,7 +10,7 @@
 
 public record S3Error(
     String code,
-    String message,
+    @Nullable String message,
     @Nullable String key,
     @Nullable String bucketName,
     @Nullable String resource,
diff --git a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3ErrorSaxHandler.java b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3ErrorSaxHandler.java
index 32894fdab..30b5b3383 100644
--- a/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3ErrorSaxHandler.java
+++ b/experimental/s3-client/src/main/java/ru/tinkoff/kora/s3/client/impl/xml/S3ErrorSaxHandler.java
@@ -75,7 +75,7 @@ public void error(SAXParseException e) throws SAXException {
     }
 
     public S3Error toResult() throws SAXException {
-        if (code == null || message == null) {
+        if (code == null) {
             throw new SAXException("Invalid response");
         }
         return new S3Error(code, message, key, bucketName, resource, requestId, hostId);