From 6e7d4a125aadc1a6452513a295cf9fe84e084759 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Mon, 3 Jul 2023 13:27:11 +0200 Subject: [PATCH] Implement Redis custom codec support Fix #34329 --- docs/src/main/asciidoc/redis-reference.adoc | 82 ++++++++++----- extensions/redis-client/deployment/pom.xml | 2 +- .../deployment/RedisDatasourceProcessor.java | 13 +++ .../datasource/CustomCodecTest.java | 92 +++++++++++++++++ .../RedisCommandExtraArguments.java | 2 +- .../redis/datasource/codecs/Codec.java | 38 ++++++- .../redis/datasource/codecs/Codecs.java | 99 +++++++++++++------ .../datasource/geo/GeoRadiusStoreArgs.java | 7 +- .../redis/datasource/geo/GeoSearchArgs.java | 5 +- .../datasource/geo/GeoSearchStoreArgs.java | 5 +- .../redis/datasource/search/QueryArgs.java | 2 +- .../runtime/client/RedisClientRecorder.java | 12 +++ .../datasource/AbstractGeoCommands.java | 4 +- .../redis/runtime/datasource/Marshaller.java | 19 ++-- .../runtime/datasource/RedisCommand.java | 2 +- integration-tests/redis-client/pom.xml | 4 +- .../quarkus/redis/it/CustomCodecResource.java | 35 +++++++ .../main/java/io/quarkus/redis/it/Person.java | 12 +++ .../java/io/quarkus/redis/it/PersonCodec.java | 29 ++++++ .../io/quarkus/redis/it/QuarkusRedisTest.java | 28 ++++++ 20 files changed, 405 insertions(+), 87 deletions(-) create mode 100644 extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/datasource/CustomCodecTest.java create mode 100644 integration-tests/redis-client/src/main/java/io/quarkus/redis/it/CustomCodecResource.java create mode 100644 integration-tests/redis-client/src/main/java/io/quarkus/redis/it/Person.java create mode 100644 integration-tests/redis-client/src/main/java/io/quarkus/redis/it/PersonCodec.java diff --git a/docs/src/main/asciidoc/redis-reference.adoc b/docs/src/main/asciidoc/redis-reference.adoc index 57d006fe1016f..f802c34766e96 100644 --- a/docs/src/main/asciidoc/redis-reference.adoc +++ b/docs/src/main/asciidoc/redis-reference.adoc @@ -21,7 +21,6 @@ Typically, we recommend: This extension provides imperative and reactive APIs and low-level and high-level (type-safe) clients. - == Installation If you want to use this extension, you need to add the `io.quarkus:quarkus-redis` extension first. @@ -86,7 +85,6 @@ To help you select the suitable API for you, here are some recommendations: * If you have existing Vert.x code, use `io.vertx.redis.client.RedisAPI` * If you need to emit custom commands, you can either use the data sources (reactive or imperative) or the `io.vertx.mutiny.redis.client.Redis`. - == Default and named clients This extension lets you configure a _default_ Redis client/data sources or _named_ ones. @@ -95,7 +93,6 @@ The latter is essential when you need to connect to multiple Redis instances. The default connection is configured using the `quarkus.redis.*` properties. For example, to configure the default Redis client, use: - [source,properties] ---- quarkus.redis.hosts=redis://localhost/ @@ -116,7 +113,6 @@ public class RedisExample { TIP: In general, you inject a single one; the previous snippet is just an example. - _Named_ clients are configured using the `quarkus.redis..*` properties: [source,properties] @@ -318,6 +314,7 @@ public class MyRedisService { } } ---- + <1> Inject the `RedisDataSource` in the constructor <2> Creates the `HashCommands` object. This object has three type parameters: the type of the key, the type of the field, and the type of the member @@ -327,10 +324,51 @@ This object has three type parameters: the type of the key, the type of the fiel === Serialization and Deserialization The data source APIs handle the serialization and deserialization automatically. -When a non-standard type is used, the object is serialized into JSON and deserialized from JSON. +By default, non-standard types are serialized into JSON and deserialized from JSON. In this case, `quarkus-jackson` is used. -To store binary data, use `byte[]`. +=== Binary + +To store or retrieve binary data, use `byte[]`. + +=== Custom codec + +You can register custom codec by providing a CDI _bean_ implementing the `io.quarkus.redis.datasource.codecs.Codec` interface: + +[source,java] +---- +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.redis.datasource.codecs.Codec; + +@ApplicationScoped +public class PersonCodec implements Codec { + @Override + public boolean canHandle(Type clazz) { + return clazz.equals(Person.class); + } + + @Override + public byte[] encode(Object item) { + var p = (Person) item; + return (p.firstName + ";" + p.lastName.toUpperCase()).getBytes(StandardCharsets.UTF_8); + } + + @Override + public Object decode(byte[] item) { + var value = new String(item, StandardCharsets.UTF_8); + var segments = value.split(";"); + return new Person(segments[0], segments[1]); + } +} +---- + +The `canHandle` method is called to check if the codec can handle a specific type. +The parameter received in the `encode` method matches that type. +The object returned by the `decode` method must also match that type. === The `value` group @@ -443,6 +481,7 @@ public static class MyRedisCounter { } ---- + <1> Retrieve the commands. This time we will manipulate `Long` values <2> Retrieve the counter associated with the given `key`. @@ -464,7 +503,7 @@ These features are available from the `pubsub` group. The following snippets shows how a _cache_ can emit a `Notification` after every `set`, and how a subscriber can receive the notification. -[source, java] +[source,java] ---- public static final class Notification { public String key; @@ -542,7 +581,7 @@ When that consumer returns, the transaction is _executed_. The following snippet shows how to create a transaction executing two related _writes_: -[source, java] +[source,java] ---- @Inject RedisDataSource ds; @@ -560,7 +599,7 @@ The returned `TransactionResult` lets you retrieve the result of each command. When using the reactive variant of the data source, the passed callback is a `Function>`: -[source, java] +[source,java] ---- @Inject ReactiveRedisDataSource ds; @@ -577,7 +616,7 @@ Transaction execution can be conditioned by _keys_. When a passed key gets modified during the execution of a transaction, the transaction is discarded. The keys are passed as `String` as a second parameter to the `withTransaction` method: -[source, java] +[source,java] ---- TransactionResult result = ds.withTransaction(tx -> { TransactionalHashCommands hash = tx.hash(String.class); @@ -608,7 +647,7 @@ EXEC For example, if you need to update a value in a hash only if the field exists, you will use the following API: -[source, java] +[source,java] ---- OptimisticLockingTransactionResult result = blocking.withTransaction(ds -> { // The pre-transaction block: @@ -641,7 +680,7 @@ The transaction is aborted if the pre-transaction block throws an exception (or To execute a custom command, or a command not supported by the API, use the following approach: -[source, java] +[source,java] ---- @Inject ReactiveRedisDataSource ds; @@ -667,7 +706,7 @@ On startup, you can configure the Redis client to preload data into the Redis da Specify the _load script_ you want to load using: -[source, properties] +[source,properties] ---- quarkus.redis.load-script=import.redis # import.redis is the default in dev mode, no-file is the default in production mode quarkus.redis.my-redis.load-script=actors.redis, movies.redis @@ -682,7 +721,7 @@ In the case of a list, the data is imported in the list order (for example, firs The `.redis` file follows a _one command per line_ format: -[source, text] +[source,text] ---- # Line starting with # and -- are ignored, as well as empty lines @@ -701,7 +740,7 @@ Quarkus batches all the commands from a single file and sends all the commands. The loading process fails if there is any error, but the previous instructions may have been executed. To avoid that, you can wrap your command into a Redis _transaction_: -[source, text] +[source,text] ---- -- Run inside a transaction MULTI @@ -725,7 +764,7 @@ You can force to import even if there is data using the `quarkus.redis.load-only As mentioned above, in dev and test modes, Quarkus tries to import data by looking for the `src/main/resources/import.redis`. This behavior is disabled in _prod_ mode, and if you want to import even in production, add: -[source, properties] +[source,properties] ---- %prod.quarkus.redis.load-script=import.redis ---- @@ -772,8 +811,8 @@ public class ExampleRedisHostProvider implements RedisHostsProvider { ---- The host provider can be used to configure the redis client like shown below -[source,properties,indent=0] +[source,properties,indent=0] ---- quarkus.redis.hosts-provider-name=hosts-provider ---- @@ -783,7 +822,7 @@ quarkus.redis.hosts-provider-name=hosts-provider You can expose a bean implementing the `io.quarkus.redis.client.RedisOptionsCustomizer` interface to customize the Redis client options. The bean is called for each configured Redis client: -[source, java] +[source,java] ---- @ApplicationScoped public static class MyExampleCustomizer implements RedisOptionsCustomizer { @@ -800,7 +839,6 @@ public static class MyExampleCustomizer implements RedisOptionsCustomizer { } ---- - === Dev Services See xref:redis-dev-services.adoc[Redis Dev Service]. @@ -814,7 +852,7 @@ Micrometer collects the metrics of all the Redis clients implemented by the appl As an example, if you export the metrics to Prometheus, you will get: -[source, text] +[source,text] ---- # HELP redis_commands_duration_seconds The duration of the operations (commands of batches # TYPE redis_commands_duration_seconds summary @@ -871,7 +909,7 @@ The metrics contain both the Redis connection pool metrics (`redis_pool_*`) and To disable the Redis client metrics when `quarkus-micrometer` is used, add the following property to the application configuration: -[source, properties] +[source,properties] ---- quarkus.micrometer.binder.redis.enabled=false ---- @@ -879,4 +917,4 @@ quarkus.micrometer.binder.redis.enabled=false [[redis-configuration-reference]] == Configuration reference -include::{generated-dir}/config/quarkus-redis-client.adoc[opts=optional, leveloffset=+1] +include::{generated-dir}/config/quarkus-redis-client.adoc[opts=optional,leveloffset=+1] diff --git a/extensions/redis-client/deployment/pom.xml b/extensions/redis-client/deployment/pom.xml index d2e0d414d5660..e18a963cfe2c4 100644 --- a/extensions/redis-client/deployment/pom.xml +++ b/extensions/redis-client/deployment/pom.xml @@ -56,7 +56,7 @@ io.quarkus - quarkus-resteasy-mutiny-deployment + quarkus-resteasy-reactive-jackson-deployment test diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisDatasourceProcessor.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisDatasourceProcessor.java index 00d9b57673e66..02cd21efcfc24 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisDatasourceProcessor.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisDatasourceProcessor.java @@ -12,9 +12,11 @@ import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; @@ -23,9 +25,11 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.redis.datasource.ReactiveRedisDataSource; import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.codecs.Codec; import io.quarkus.redis.runtime.client.RedisClientRecorder; import io.quarkus.vertx.deployment.VertxBuildItem; @@ -65,6 +69,15 @@ public void detectUsage(BuildProducer request, } } + @BuildStep + public void makeCodecsUnremovable(CombinedIndexBuildItem index, BuildProducer producer) { + producer.produce(AdditionalBeanBuildItem.unremovableOf(Codec.class)); + + for (ClassInfo implementor : index.getIndex().getAllKnownImplementors(Codec.class.getName())) { + producer.produce(AdditionalBeanBuildItem.unremovableOf(implementor.name().toString())); + } + } + @BuildStep @Record(ExecutionTime.RUNTIME_INIT) public void init(RedisClientRecorder recorder, diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/datasource/CustomCodecTest.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/datasource/CustomCodecTest.java new file mode 100644 index 0000000000000..5ceb364a6d820 --- /dev/null +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/datasource/CustomCodecTest.java @@ -0,0 +1,92 @@ +package io.quarkus.redis.client.deployment.datasource; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.redis.client.deployment.RedisTestResource; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.codecs.Codec; +import io.quarkus.redis.datasource.codecs.Codecs; +import io.quarkus.redis.datasource.hash.HashCommands; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; + +@QuarkusTestResource(RedisTestResource.class) +public class CustomCodecTest { + + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class).addClass(Jedi.class).addClass(MyCustomCodec.class)) + .overrideConfigKey("quarkus.redis.hosts", "${quarkus.redis.tr}"); + + @Inject + RedisDataSource ds; + + @Test + void testCustomCodecs() { + String key1 = UUID.randomUUID().toString(); + // Check that the codec is registered + assertThat(Codecs.getDefaultCodecFor(Jedi.class)).isInstanceOf(MyCustomCodec.class); + + HashCommands hash1 = ds.hash(Jedi.class); + hash1.hset(key1, "test", new Jedi("luke", "skywalker")); + var retrieved = hash1.hget(key1, "test"); + assertThat(retrieved.firstName).isEqualTo("luke"); + assertThat(retrieved.lastName).isEqualTo("SKYWALKER"); + + HashCommands hash2 = ds.hash(String.class, Jedi.class, String.class); + hash2.hset(key1, new Jedi("luke", "skywalker"), "test"); + var retrieved2 = hash2.hget(key1, new Jedi("luke", "skywalker")); + assertThat(retrieved2).isEqualTo("test"); + + HashCommands hash3 = ds.hash(Jedi.class, String.class, String.class); + hash3.hset(new Jedi("luke", "skywalker"), "key", "value"); + var retrieved3 = hash3.hget(new Jedi("luke", "skywalker"), "key"); + assertThat(retrieved3).isEqualTo("value"); + } + + @ApplicationScoped + public static class MyCustomCodec implements Codec { + + @Override + public boolean canHandle(Type clazz) { + return clazz.equals(Jedi.class); + } + + @Override + public byte[] encode(Object item) { + var jedi = (Jedi) item; + return (jedi.firstName + ";" + jedi.lastName).getBytes(StandardCharsets.UTF_8); + } + + @Override + public Object decode(byte[] item) { + String s = new String(item, StandardCharsets.UTF_8); + String[] strings = s.split(";"); + return new Jedi(strings[0], strings[1].toUpperCase()); + } + } + + public static class Jedi { + public final String firstName; + public final String lastName; + + public Jedi(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + } + +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisCommandExtraArguments.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisCommandExtraArguments.java index d14aa05a689f7..3d58d049a4dc3 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisCommandExtraArguments.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisCommandExtraArguments.java @@ -18,7 +18,7 @@ default List toArgs() { * @param encoder an optional encoder to encode some of the values * @return the list of arguments, encoded as a list of String. */ - default List toArgs(Codec encoder) { + default List toArgs(Codec encoder) { return Collections.emptyList(); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/codecs/Codec.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/codecs/Codec.java index 6b4989db96619..0facc4c0b335b 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/codecs/Codec.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/codecs/Codec.java @@ -1,9 +1,41 @@ package io.quarkus.redis.datasource.codecs; -public interface Codec { +import java.lang.reflect.Type; - byte[] encode(T item); +/** + * Redis codec interface. + *

+ * The Redis data source uses this interface to serialize and deserialize data to and from Redis. + * A set of default codecs are provided for Strings, Integers, Doubles and Byte arrays. + * For custom types, either there is a specific implementation of this interface exposed as CDI (Application Scoped) bean, + * or JSON is used. + */ +public interface Codec { - T decode(byte[] item); + /** + * Checks if the current codec can handle the serialization and deserialization of object from the given type. + * + * @param clazz the type, cannot be {@code null} + * @return {@code true} if the codec can handle the type, {@code false} otherwise + */ + boolean canHandle(Type clazz); + + /** + * Encodes the given object. + * The type of the given object matches the type used to call the {@link #canHandle(Type)} method. + * + * @param item the item + * @return the encoded content + */ + byte[] encode(Object item); + + /** + * Decodes the given bytes to an object. + * The codec must return an instance of the type used to call the {@link #canHandle(Type)} method. + * + * @param item the bytes + * @return the object + */ + Object decode(byte[] item); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/codecs/Codecs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/codecs/Codecs.java index 4c99de87a09ad..f7fc7ceb4c764 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/codecs/Codecs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/codecs/Codecs.java @@ -1,6 +1,11 @@ package io.quarkus.redis.datasource.codecs; +import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Stream; import io.vertx.core.buffer.Buffer; import io.vertx.core.json.Json; @@ -11,44 +16,54 @@ private Codecs() { // Avoid direct instantiation } - @SuppressWarnings("unchecked") - public static Codec getDefaultCodecFor(Class clazz) { - if (clazz.equals(Double.class) || clazz.equals(Double.TYPE)) { - return (Codec) DoubleCodec.INSTANCE; - } - if (clazz.equals(Integer.class) || clazz.equals(Integer.TYPE)) { - return (Codec) IntegerCodec.INSTANCE; - } - if (clazz.equals(String.class)) { - return (Codec) StringCodec.INSTANCE; - } - if (clazz.equals(byte[].class)) { - return (Codec) ByteArrayCodec.INSTANCE; + private static final List CODECS = new CopyOnWriteArrayList<>( + List.of(StringCodec.INSTANCE, DoubleCodec.INSTANCE, IntegerCodec.INSTANCE, ByteArrayCodec.INSTANCE)); + + public static void register(Codec codec) { + CODECS.add(Objects.requireNonNull(codec)); + } + + public static void register(Stream codecs) { + codecs.forEach(Codecs::register); + } + + public static Codec getDefaultCodecFor(Type type) { + for (Codec codec : CODECS) { + if (codec.canHandle(type)) { + return codec; + } } + // JSON by default - return new JsonCodec<>(clazz); + return new JsonCodec(type); } - public static class JsonCodec implements Codec { + public static class JsonCodec implements Codec { - private final Class clazz; + private final Type clazz; - public JsonCodec(Class clazz) { + public JsonCodec(Type clazz) { this.clazz = clazz; } @Override - public byte[] encode(T item) { + public boolean canHandle(Type clazz) { + throw new UnsupportedOperationException("Should not be called, the JSON codec is the fallback"); + } + + @Override + public byte[] encode(Object item) { return Json.encodeToBuffer(item).getBytes(); } @Override - public T decode(byte[] payload) { - return Json.decodeValue(Buffer.buffer(payload), clazz); + public Object decode(byte[] payload) { + // TODO This would need to be revisited when we use TypeReference. + return Json.decodeValue(Buffer.buffer(payload), (Class) clazz); } } - public static class StringCodec implements Codec { + public static class StringCodec implements Codec { public static StringCodec INSTANCE = new StringCodec(); @@ -57,8 +72,13 @@ private StringCodec() { } @Override - public byte[] encode(String item) { - return item.getBytes(StandardCharsets.UTF_8); + public boolean canHandle(Type clazz) { + return clazz.equals(String.class); + } + + @Override + public byte[] encode(Object item) { + return ((String) item).getBytes(StandardCharsets.UTF_8); } @Override @@ -67,7 +87,7 @@ public String decode(byte[] item) { } } - public static class DoubleCodec implements Codec { + public static class DoubleCodec implements Codec { public static DoubleCodec INSTANCE = new DoubleCodec(); @@ -76,11 +96,16 @@ private DoubleCodec() { } @Override - public byte[] encode(Double item) { + public boolean canHandle(Type clazz) { + return clazz.equals(Double.class) || clazz.equals(Double.TYPE); + } + + @Override + public byte[] encode(Object item) { if (item == null) { return null; } - return Double.toString(item).getBytes(StandardCharsets.UTF_8); + return Double.toString((double) item).getBytes(StandardCharsets.UTF_8); } @Override @@ -92,7 +117,7 @@ public Double decode(byte[] item) { } } - public static class IntegerCodec implements Codec { + public static class IntegerCodec implements Codec { public static IntegerCodec INSTANCE = new IntegerCodec(); @@ -101,11 +126,16 @@ private IntegerCodec() { } @Override - public byte[] encode(Integer item) { + public boolean canHandle(Type clazz) { + return clazz.equals(Integer.class) || clazz.equals(Integer.TYPE); + } + + @Override + public byte[] encode(Object item) { if (item == null) { return null; } - return Integer.toString(item).getBytes(StandardCharsets.UTF_8); + return Integer.toString((int) item).getBytes(StandardCharsets.UTF_8); } @Override @@ -117,7 +147,7 @@ public Integer decode(byte[] item) { } } - public static class ByteArrayCodec implements Codec { + public static class ByteArrayCodec implements Codec { public static ByteArrayCodec INSTANCE = new ByteArrayCodec(); @@ -126,8 +156,13 @@ private ByteArrayCodec() { } @Override - public byte[] encode(byte[] item) { - return item; + public boolean canHandle(Type clazz) { + return clazz.equals(byte[].class); + } + + @Override + public byte[] encode(Object item) { + return (byte[]) item; } @Override diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusStoreArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusStoreArgs.java index bc75d8d2c9ab6..e1e5b61a6328f 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusStoreArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusStoreArgs.java @@ -126,9 +126,8 @@ public GeoRadiusStoreArgs storeDistKey(K storeDistKey) { return this; } - @SuppressWarnings("unchecked") @Override - public List toArgs(Codec codec) { + public List toArgs(Codec codec) { // Validation if (any && count == -1) { throw new IllegalArgumentException("ANY can only be used if COUNT is also set"); @@ -161,12 +160,12 @@ public List toArgs(Codec codec) { if (storeKey != null) { list.add("STORE"); - list.add(new String(codec.encode((T) storeKey), StandardCharsets.UTF_8)); + list.add(new String(codec.encode(storeKey), StandardCharsets.UTF_8)); } if (storeDistKey != null) { list.add("STOREDIST"); - list.add(new String(codec.encode((T) storeDistKey), StandardCharsets.UTF_8)); + list.add(new String(codec.encode(storeDistKey), StandardCharsets.UTF_8)); } return list; diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchArgs.java index 29b2cd9140f56..8d0c9f851c121 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchArgs.java @@ -183,9 +183,8 @@ public GeoSearchArgs any() { return this; } - @SuppressWarnings("unchecked") @Override - public List toArgs(Codec codec) { + public List toArgs(Codec codec) { // Validation if (any && count == -1) { throw new IllegalArgumentException("ANY can only be used if COUNT is also set"); @@ -202,7 +201,7 @@ public List toArgs(Codec codec) { List list = new ArrayList<>(); if (member != null) { list.add("FROMMEMBER"); - list.add(new String(codec.encode((T) member), StandardCharsets.UTF_8)); + list.add(new String(codec.encode(member), StandardCharsets.UTF_8)); } else { list.add("FROMLONLAT"); list.add(Double.toString(longitude)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchStoreArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchStoreArgs.java index 5443316b8ff13..fe9057eff57f8 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchStoreArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchStoreArgs.java @@ -145,9 +145,8 @@ public GeoSearchStoreArgs any() { return this; } - @SuppressWarnings("unchecked") @Override - public List toArgs(Codec codec) { + public List toArgs(Codec codec) { // Validation if (any && count == -1) { throw new IllegalArgumentException("ANY can only be used if COUNT is also set"); @@ -164,7 +163,7 @@ public List toArgs(Codec codec) { List list = new ArrayList<>(); if (member != null) { list.add("FROMMEMBER"); - list.add(new String(codec.encode((T) member), StandardCharsets.UTF_8)); + list.add(new String(codec.encode(member), StandardCharsets.UTF_8)); } else { list.add("FROMLONLAT"); list.add(Double.toString(longitude)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java index 9210b0757a01d..76d229440d001 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java @@ -372,7 +372,7 @@ public QueryArgs dialect(int version) { } @Override - public List toArgs(Codec encoder) { + public List toArgs(Codec encoder) { List list = new ArrayList<>(); if (nocontent) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisClientRecorder.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisClientRecorder.java index a649952c0f322..0fa2c0c432c16 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisClientRecorder.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisClientRecorder.java @@ -17,6 +17,8 @@ import io.quarkus.redis.client.reactive.ReactiveRedisClient; import io.quarkus.redis.datasource.ReactiveRedisDataSource; import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.codecs.Codec; +import io.quarkus.redis.datasource.codecs.Codecs; import io.quarkus.redis.runtime.client.config.RedisClientConfig; import io.quarkus.redis.runtime.client.config.RedisConfig; import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; @@ -55,9 +57,19 @@ public void initialize(RuntimeValue vertx, Set name } this.vertx = Vertx.newInstance(vertx.getValue()); + + _registerCodecs(); + _initialize(vertx.getValue(), names); } + private static void _registerCodecs() { + Instance codecs = CDI.current().select(Codec.class); + if (codecs.isResolvable()) { + Codecs.register(codecs.stream()); + } + } + public void _initialize(io.vertx.core.Vertx vertx, Set names) { for (String name : names) { // Search if we have an associated config: diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractGeoCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractGeoCommands.java index 04367f7dfb73a..47841954c31be 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractGeoCommands.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractGeoCommands.java @@ -32,8 +32,8 @@ class AbstractGeoCommands extends AbstractRedisCommands { protected final Class typeOfValue; - protected final Codec keyCodec; - protected final Codec valueCodec; + protected final Codec keyCodec; + protected final Codec valueCodec; private static final Pattern NOISE_REMOVER_PATTERN = Pattern.compile("[^a-zA-Z0-9\\.]"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Marshaller.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Marshaller.java index c64fa7d521f62..74bf0ca39540d 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Marshaller.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Marshaller.java @@ -22,7 +22,7 @@ public class Marshaller { - private static final Map, Codec> DEFAULT_CODECS; + private static final Map, Codec> DEFAULT_CODECS; static { DEFAULT_CODECS = Map.of( @@ -31,7 +31,7 @@ public class Marshaller { Double.class, Codecs.DoubleCodec.INSTANCE); } - Map, Codec> codecs = new ConcurrentHashMap<>(); + Map, Codec> codecs = new ConcurrentHashMap<>(); public Marshaller(Class... hints) { addAll(hints); @@ -48,7 +48,6 @@ public void add(Class hint) { codecs.computeIfAbsent(hint, h -> Codecs.getDefaultCodecFor(hint)); } - @SuppressWarnings({ "rawtypes", "unchecked" }) public byte[] encode(Object o) { if (o instanceof String) { return ((String) o).getBytes(StandardCharsets.UTF_8); @@ -58,11 +57,6 @@ public byte[] encode(Object o) { } Class clazz = o.getClass(); Codec codec = codec(clazz); - if (codec == null) { - // Default to JSON. - codec = new Codecs.JsonCodec<>(clazz); - codecs.put(clazz, codec); - } return codec.encode(o); } @@ -77,10 +71,11 @@ public final List encode(T... objects) { return result; } - Codec codec(Class clazz) { - Codec codec = codecs.get(clazz); + Codec codec(Class clazz) { + Codec codec = codecs.get(clazz); if (codec == null) { - codec = DEFAULT_CODECS.get(clazz); + codec = Codecs.getDefaultCodecFor(clazz); + codecs.put(clazz, codec); } return codec; } @@ -100,7 +95,7 @@ public final T decode(Class clazz, byte[] r) { if (r == null) { return null; } - Codec codec = codec(clazz); + Codec codec = codec(clazz); return (T) codec.decode(r); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/RedisCommand.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/RedisCommand.java index d21b48131cceb..06be66cecb734 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/RedisCommand.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/RedisCommand.java @@ -66,7 +66,7 @@ public RedisCommand putArgs(RedisCommandExtraArguments arguments) { return this; } - public RedisCommand putArgs(RedisCommandExtraArguments arguments, Codec codec) { + public RedisCommand putArgs(RedisCommandExtraArguments arguments, Codec codec) { putAll(arguments.toArgs(codec)); return this; } diff --git a/integration-tests/redis-client/pom.xml b/integration-tests/redis-client/pom.xml index ac768ecea000e..34b5e83c23917 100644 --- a/integration-tests/redis-client/pom.xml +++ b/integration-tests/redis-client/pom.xml @@ -18,7 +18,7 @@ io.quarkus - quarkus-resteasy-reactive + quarkus-resteasy-reactive-jackson io.quarkus @@ -57,7 +57,7 @@ io.quarkus - quarkus-resteasy-reactive-deployment + quarkus-resteasy-reactive-jackson-deployment ${project.version} pom test diff --git a/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/CustomCodecResource.java b/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/CustomCodecResource.java new file mode 100644 index 0000000000000..119817d0d0775 --- /dev/null +++ b/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/CustomCodecResource.java @@ -0,0 +1,35 @@ +package io.quarkus.redis.it; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.value.ValueCommands; + +@Path("/quarkus-redis/custom-codec") +@ApplicationScoped +public class CustomCodecResource { + + private final ValueCommands values; + + public CustomCodecResource(RedisDataSource ds) { + values = ds.value(Person.class); + } + + // synchronous + @GET + @Path("/{key}") + public Person getSync(@PathParam("key") String key) { + return values.get(key); + } + + @POST + @Path("/{key}") + public void setSync(@PathParam("key") String key, Person value) { + values.set(key, value); + } + +} diff --git a/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/Person.java b/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/Person.java new file mode 100644 index 0000000000000..4d62c82122ef9 --- /dev/null +++ b/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/Person.java @@ -0,0 +1,12 @@ +package io.quarkus.redis.it; + +public class Person { + + public final String firstName; + public final String lastName; + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } +} diff --git a/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/PersonCodec.java b/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/PersonCodec.java new file mode 100644 index 0000000000000..248a05713c430 --- /dev/null +++ b/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/PersonCodec.java @@ -0,0 +1,29 @@ +package io.quarkus.redis.it; + +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.redis.datasource.codecs.Codec; + +@ApplicationScoped +public class PersonCodec implements Codec { + @Override + public boolean canHandle(Type clazz) { + return clazz.equals(Person.class); + } + + @Override + public byte[] encode(Object item) { + var p = (Person) item; + return (p.firstName + ";" + p.lastName.toUpperCase()).getBytes(StandardCharsets.UTF_8); + } + + @Override + public Object decode(byte[] item) { + var value = new String(item, StandardCharsets.UTF_8); + var segments = value.split(";"); + return new Person(segments[0], segments[1]); + } +} diff --git a/integration-tests/redis-client/src/test/java/io/quarkus/redis/it/QuarkusRedisTest.java b/integration-tests/redis-client/src/test/java/io/quarkus/redis/it/QuarkusRedisTest.java index a8d2f27659de3..c2dd33be49cef 100644 --- a/integration-tests/redis-client/src/test/java/io/quarkus/redis/it/QuarkusRedisTest.java +++ b/integration-tests/redis-client/src/test/java/io/quarkus/redis/it/QuarkusRedisTest.java @@ -2,6 +2,7 @@ import org.hamcrest.CoreMatchers; import org.hamcrest.Matchers; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; @@ -86,4 +87,31 @@ public void testPreloading() { .statusCode(200) .body(Matchers.equalTo("6")); } + + @Test + public void testCustomCodec() { + String path = "/quarkus-redis/custom-codec/foo"; + RestAssured.given() + .when() + .get(path) + .then() + .statusCode(204); // the key is not set yet + + RestAssured.given() + .header("Content-Type", "application/json") + .body(new Person("bob", "morane")) + .when() + .post(path) + .then() + .statusCode(204); + + var person = RestAssured.given() + .when() + .get(path) + .then() + .statusCode(200) + .extract().as(Person.class); + Assertions.assertEquals(person.firstName, "bob"); + Assertions.assertEquals(person.lastName, "MORANE"); + } }