diff --git a/extensions/redis-cache/deployment/src/main/java/io/quarkus/cache/redis/deployment/RedisCacheProcessor.java b/extensions/redis-cache/deployment/src/main/java/io/quarkus/cache/redis/deployment/RedisCacheProcessor.java index 0dbce4bf6bfd4..a9c0c0817cd19 100644 --- a/extensions/redis-cache/deployment/src/main/java/io/quarkus/cache/redis/deployment/RedisCacheProcessor.java +++ b/extensions/redis-cache/deployment/src/main/java/io/quarkus/cache/redis/deployment/RedisCacheProcessor.java @@ -146,21 +146,19 @@ private static Map valueTypesFromCacheResultAnnotation(CombinedI } Type type = typeSet.iterator().next(); - String resolvedType = null; - if (type.kind() == Type.Kind.CLASS) { - resolvedType = type.asClassType().name().toString(); - } else if (type.kind() == Type.Kind.PRIMITIVE) { - resolvedType = type.asPrimitiveType().name().toString(); - } else if ((type.kind() == Type.Kind.PARAMETERIZED_TYPE) && UNI.equals(type.name())) { + Type resolvedType = null; + if (type.kind() == Type.Kind.PARAMETERIZED_TYPE && UNI.equals(type.name())) { ParameterizedType parameterizedType = type.asParameterizedType(); List arguments = parameterizedType.arguments(); if (arguments.size() == 1) { - resolvedType = arguments.get(0).name().toString(); + resolvedType = arguments.get(0); } + } else { + resolvedType = type; } if (resolvedType != null) { - result.put(cacheName, resolvedType); + result.put(cacheName, resolvedType.toString()); // TODO type annotations?! } else { LOGGER.debugv( "Cache named '{0}' is used on method whose return type '{1}' is not eligible for automatic resolution", diff --git a/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ComplexCachedService.java b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ComplexCachedService.java new file mode 100644 index 0000000000000..53117d8bb19c0 --- /dev/null +++ b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ComplexCachedService.java @@ -0,0 +1,28 @@ +package io.quarkus.cache.redis.deployment; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.cache.CacheResult; + +@ApplicationScoped +public class ComplexCachedService { + static final String CACHE_NAME_GENERIC = "test-cache-generic"; + static final String CACHE_NAME_ARRAY = "test-cache-array"; + + @CacheResult(cacheName = CACHE_NAME_GENERIC) + public List genericReturnType(String key) { + return List.of(UUID.randomUUID().toString(), UUID.randomUUID().toString()); + } + + @CacheResult(cacheName = CACHE_NAME_ARRAY) + public int[] arrayReturnType(String key) { + int[] result = new int[2]; + result[0] = ThreadLocalRandom.current().nextInt(); + result[1] = ThreadLocalRandom.current().nextInt(); + return result; + } +} diff --git a/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ComplexTypesRedisCacheTest.java b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ComplexTypesRedisCacheTest.java new file mode 100644 index 0000000000000..c5742823ad4bf --- /dev/null +++ b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ComplexTypesRedisCacheTest.java @@ -0,0 +1,78 @@ +package io.quarkus.cache.redis.deployment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class ComplexTypesRedisCacheTest { + private static final String KEY_1 = "1"; + private static final String KEY_2 = "2"; + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(ComplexCachedService.class, TestUtil.class)); + + @Inject + ComplexCachedService cachedService; + + @Test + public void testGeneric() { + RedisDataSource redisDataSource = Arc.container().select(RedisDataSource.class).get(); + List allKeysAtStart = TestUtil.allRedisKeys(redisDataSource); + + // STEP 1 + // Action: @CacheResult-annotated method call. + // Expected effect: method invoked and result cached. + // Verified by: STEP 2. + List value1 = cachedService.genericReturnType(KEY_1); + List newKeys = TestUtil.allRedisKeys(redisDataSource); + assertEquals(allKeysAtStart.size() + 1, newKeys.size()); + assertThat(newKeys).contains(expectedCacheKey(ComplexCachedService.CACHE_NAME_GENERIC, KEY_1)); + + // STEP 2 + // Action: same call as STEP 1. + // Expected effect: method not invoked and result coming from the cache. + // Verified by: same object reference between STEPS 1 and 2 results. + List value2 = cachedService.genericReturnType(KEY_1); + assertEquals(value1, value2); + assertEquals(allKeysAtStart.size() + 1, TestUtil.allRedisKeys(redisDataSource).size()); + } + + @Test + public void testArray() { + RedisDataSource redisDataSource = Arc.container().select(RedisDataSource.class).get(); + List allKeysAtStart = TestUtil.allRedisKeys(redisDataSource); + + // STEP 1 + // Action: @CacheResult-annotated method call. + // Expected effect: method invoked and result cached. + // Verified by: STEP 2. + int[] value1 = cachedService.arrayReturnType(KEY_2); + List newKeys = TestUtil.allRedisKeys(redisDataSource); + assertEquals(allKeysAtStart.size() + 1, newKeys.size()); + assertThat(newKeys).contains(expectedCacheKey(ComplexCachedService.CACHE_NAME_ARRAY, KEY_2)); + + // STEP 2 + // Action: same call as STEP 1. + // Expected effect: method not invoked and result coming from the cache. + // Verified by: same object reference between STEPS 1 and 2 results. + int[] value2 = cachedService.arrayReturnType(KEY_2); + assertArrayEquals(value1, value2); + assertEquals(allKeysAtStart.size() + 1, TestUtil.allRedisKeys(redisDataSource).size()); + } + + private static String expectedCacheKey(String cacheName, String key) { + return "cache:" + cacheName + ":" + key; + } +} diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCache.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCache.java index 8127fde03e743..edccfae1b3711 100644 --- a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCache.java +++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCache.java @@ -1,5 +1,6 @@ package io.quarkus.cache.redis.runtime; +import java.lang.reflect.Type; import java.util.function.Function; import java.util.function.Supplier; @@ -8,17 +9,28 @@ public interface RedisCache extends Cache { + /** + * When configured, gets the default type of the value stored in the cache. + * The configured type is used when no type is passed into the {@link #get(Object, Class, Function)}. + * + * @return the type, {@code null} if not configured or if not a {@code Class}. + */ + default Class getDefaultValueType() { + Type type = getDefaultValueGenericType(); + return type instanceof Class ? (Class) type : null; + } + /** * When configured, gets the default type of the value stored in the cache. * The configured type is used when no type is passed into the {@link #get(Object, Class, Function)}. * * @return the type, {@code null} if not configured. */ - Class getDefaultValueType(); + Type getDefaultValueGenericType(); @Override default Uni get(K key, Function valueLoader) { - Class type = (Class) getDefaultValueType(); + Type type = getDefaultValueGenericType(); if (type == null) { throw new UnsupportedOperationException("Cannot use `get` method without a default type configured. " + "Consider using the `get` method accepting the type or configure the default type for the cache " + @@ -30,7 +42,7 @@ default Uni get(K key, Function valueLoader) { @SuppressWarnings("unchecked") @Override default Uni getAsync(K key, Function> valueLoader) { - Class type = (Class) getDefaultValueType(); + Type type = getDefaultValueGenericType(); if (type == null) { throw new UnsupportedOperationException("Cannot use `getAsync` method without a default type configured. " + "Consider using the `getAsync` method accepting the type or configure the default type for the cache " + @@ -49,7 +61,21 @@ default Uni getAsync(K key, Function> valueLoader) { * @param the type of value * @return the Uni emitting the cached value. */ - Uni get(K key, Class clazz, Function valueLoader); + default Uni get(K key, Class clazz, Function valueLoader) { + return get(key, (Type) clazz, valueLoader); + } + + /** + * Allows retrieving a value from the Redis cache. + * + * @param key the key + * @param type the type of the value + * @param valueLoader the value loader called when there is no value stored in the cache + * @param the type of key + * @param the type of value + * @return the Uni emitting the cached value. + */ + Uni get(K key, Type type, Function valueLoader); /** * Allows retrieving a value from the Redis cache. @@ -61,7 +87,21 @@ default Uni getAsync(K key, Function> valueLoader) { * @param the type of value * @return the Uni emitting the cached value. */ - Uni getAsync(K key, Class clazz, Function> valueLoader); + default Uni getAsync(K key, Class clazz, Function> valueLoader) { + return getAsync(key, (Type) clazz, valueLoader); + } + + /** + * Allows retrieving a value from the Redis cache. + * + * @param key the key + * @param type the type of the value + * @param valueLoader the value loader called when there is no value stored in the cache + * @param the type of key + * @param the type of value + * @return the Uni emitting the cached value. + */ + Uni getAsync(K key, Type type, Function> valueLoader); /** * Put a value in the cache. @@ -85,5 +125,9 @@ public V get() { Uni getOrDefault(K key, V defaultValue); - Uni getOrNull(K key, Class clazz); + default Uni getOrNull(K key, Class clazz) { + return getOrNull(key, (Type) clazz); + } + + Uni getOrNull(K key, Type type); } diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheBuildRecorder.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheBuildRecorder.java index a55f128de6fbf..a753be157be4c 100644 --- a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheBuildRecorder.java +++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheBuildRecorder.java @@ -35,7 +35,6 @@ public boolean supports(Context context) { } @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) public Supplier get(Context context) { return new Supplier() { @Override diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java index d0be182e72ad0..ac9e4a8a2da8f 100644 --- a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java +++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java @@ -1,10 +1,10 @@ package io.quarkus.cache.redis.runtime; +import java.lang.reflect.Type; import java.net.ConnectException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Function; @@ -41,22 +41,12 @@ public class RedisCacheImpl extends AbstractCache implements RedisCache { private static final Logger log = Logger.getLogger(RedisCacheImpl.class); - private static final Map> PRIMITIVE_TO_CLASS_MAPPING = Map.of( - "int", Integer.class, - "byte", Byte.class, - "char", Character.class, - "short", Short.class, - "long", Long.class, - "float", Float.class, - "double", Double.class, - "boolean", Boolean.class); - private final Vertx vertx; private final Redis redis; private final RedisCacheInfo cacheInfo; - private final Class classOfValue; - private final Class classOfKey; + private final Type classOfValue; + private final Type classOfKey; private final Marshaller marshaller; @@ -82,18 +72,10 @@ public RedisCacheImpl(RedisCacheInfo cacheInfo, Vertx vertx, Redis redis, Suppli this.cacheInfo = cacheInfo; this.blockingAllowedSupplier = blockingAllowedSupplier; - try { - this.classOfKey = loadClass(this.cacheInfo.keyType); - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException("Unable to load the class " + this.cacheInfo.keyType, e); - } + this.classOfKey = TypeParser.parse(this.cacheInfo.keyType); if (this.cacheInfo.valueType != null) { - try { - this.classOfValue = loadClass(this.cacheInfo.valueType); - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException("Unable to load the class " + this.cacheInfo.valueType, e); - } + this.classOfValue = TypeParser.parse(this.cacheInfo.valueType); this.marshaller = new Marshaller(this.classOfValue, this.classOfKey); } else { this.classOfValue = null; @@ -108,13 +90,6 @@ private static boolean isRecomputableError(Throwable error) { || error instanceof ConnectionPoolTooBusyException; } - private Class loadClass(String type) throws ClassNotFoundException { - if (PRIMITIVE_TO_CLASS_MAPPING.containsKey(type)) { - return PRIMITIVE_TO_CLASS_MAPPING.get(type); - } - return Thread.currentThread().getContextClassLoader().loadClass(type); - } - @Override public String getName() { return Objects.requireNonNullElse(cacheInfo.name, "default-redis-cache"); @@ -126,7 +101,7 @@ public Object getDefaultKey() { } @Override - public Class getDefaultValueType() { + public Type getDefaultValueGenericType() { return classOfValue; } @@ -148,7 +123,7 @@ public V get() { } @Override - public Uni get(K key, Class clazz, Function valueLoader) { + public Uni get(K key, Type type, Function valueLoader) { // With optimistic locking: // WATCH K // val = deserialize(GET K) @@ -171,9 +146,9 @@ public Uni apply(RedisConnection connection) { Uni startingPoint; if (cacheInfo.useOptimisticLocking) { startingPoint = watch(connection, encodedKey) - .chain(new GetFromConnectionSupplier<>(connection, clazz, encodedKey, marshaller)); + .chain(new GetFromConnectionSupplier<>(connection, type, encodedKey, marshaller)); } else { - startingPoint = new GetFromConnectionSupplier<>(connection, clazz, encodedKey, marshaller).get(); + startingPoint = new GetFromConnectionSupplier(connection, type, encodedKey, marshaller).get(); } return startingPoint @@ -227,7 +202,7 @@ public Uni apply(Throwable e) { } @Override - public Uni getAsync(K key, Class clazz, Function> valueLoader) { + public Uni getAsync(K key, Type type, Function> valueLoader) { byte[] encodedKey = marshaller.encode(computeActualKey(encodeKey(key))); return withConnection(new Function>() { @Override @@ -235,9 +210,9 @@ public Uni apply(RedisConnection connection) { Uni startingPoint; if (cacheInfo.useOptimisticLocking) { startingPoint = watch(connection, encodedKey) - .chain(new GetFromConnectionSupplier<>(connection, clazz, encodedKey, marshaller)); + .chain(new GetFromConnectionSupplier<>(connection, type, encodedKey, marshaller)); } else { - startingPoint = new GetFromConnectionSupplier<>(connection, clazz, encodedKey, marshaller).get(); + startingPoint = new GetFromConnectionSupplier(connection, type, encodedKey, marshaller).get(); } return startingPoint @@ -296,7 +271,6 @@ private void enforceDefaultType() { } } - @SuppressWarnings("unchecked") @Override public Uni getOrDefault(K key, V defaultValue) { enforceDefaultType(); @@ -304,20 +278,19 @@ public Uni getOrDefault(K key, V defaultValue) { return withConnection(new Function>() { @Override public Uni apply(RedisConnection redisConnection) { - return (Uni) doGet(redisConnection, encodedKey, classOfValue, marshaller); + return doGet(redisConnection, encodedKey, classOfValue, marshaller); } }).onItem().ifNull().continueWith(new StaticSupplier<>(defaultValue)); } @Override - @SuppressWarnings("unchecked") - public Uni getOrNull(K key, Class clazz) { + public Uni getOrNull(K key, Type type) { enforceDefaultType(); byte[] encodedKey = marshaller.encode(computeActualKey(encodeKey(key))); return withConnection(new Function>() { @Override public Uni apply(RedisConnection redisConnection) { - return (Uni) doGet(redisConnection, encodedKey, classOfValue, marshaller); + return doGet(redisConnection, encodedKey, classOfValue, marshaller); } }); } @@ -408,7 +381,7 @@ private Uni watch(RedisConnection connection, byte[] keyToWatch) { .replaceWithVoid(); } - private Uni doGet(RedisConnection connection, byte[] encoded, Class clazz, + private Uni doGet(RedisConnection connection, byte[] encoded, Type clazz, Marshaller marshaller) { if (cacheInfo.expireAfterAccess.isPresent()) { Duration duration = cacheInfo.expireAfterAccess.get(); @@ -470,11 +443,11 @@ public V get() { private class GetFromConnectionSupplier implements Supplier> { private final RedisConnection connection; - private final Class clazz; + private final Type clazz; private final byte[] encodedKey; private final Marshaller marshaller; - public GetFromConnectionSupplier(RedisConnection connection, Class clazz, byte[] encodedKey, Marshaller marshaller) { + public GetFromConnectionSupplier(RedisConnection connection, Type clazz, byte[] encodedKey, Marshaller marshaller) { this.connection = connection; this.clazz = clazz; this.encodedKey = encodedKey; diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/TypeParser.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/TypeParser.java new file mode 100644 index 0000000000000..31d9d0a2272c8 --- /dev/null +++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/TypeParser.java @@ -0,0 +1,226 @@ +package io.quarkus.cache.redis.runtime; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.quarkus.cache.redis.runtime.impl.GenericArrayTypeImpl; +import io.quarkus.cache.redis.runtime.impl.ParameterizedTypeImpl; +import io.quarkus.cache.redis.runtime.impl.WildcardTypeImpl; + +/** + * Creates a {@link Type} by parsing the given string according to the following grammar: + * + *
+ * Type -> VoidType | PrimitiveType | ReferenceType
+ * VoidType -> 'void'
+ * PrimitiveType -> 'boolean' | 'byte' | 'short' | 'int'
+ *                | 'long' | 'float' | 'double' | 'char'
+ * ReferenceType -> PrimitiveType ('[' ']')+
+ *                | ClassType ('<' TypeArgument (',' TypeArgument)* '>')? ('[' ']')*
+ * ClassType -> FULLY_QUALIFIED_NAME
+ * TypeArgument -> ReferenceType | WildcardType
+ * WildcardType -> '?' | '?' ('extends' | 'super') ReferenceType
+ * 
+ * + * Notice that the resulting type never contains type variables, only "proper" types. + * Also notice that the grammar above does not support all kinds of nested types; + * it should be possible to add that later, if there's an actual need. + */ +class TypeParser { + static Type parse(String str) { + return new TypeParser(str).parse(); + } + + private final String str; + + private int pos = 0; + + TypeParser(String str) { + this.str = Objects.requireNonNull(str); + } + + Type parse() { + Type result; + + String token = nextToken(); + if (token.isEmpty()) { + throw unexpected(token); + } else if (token.equals("void")) { + result = void.class; + } else if (isPrimitiveType(token) && peekToken().isEmpty()) { + result = parsePrimitiveType(token); + } else { + result = parseReferenceType(token); + } + + expect(""); + return result; + } + + private Type parseReferenceType(String token) { + if (isPrimitiveType(token)) { + Type primitive = parsePrimitiveType(token); + return parseArrayType(primitive); + } else if (isClassType(token)) { + Type result = parseClassType(token); + if (peekToken().equals("<")) { + expect("<"); + List typeArguments = new ArrayList<>(); + typeArguments.add(parseTypeArgument()); + while (peekToken().equals(",")) { + expect(","); + typeArguments.add(parseTypeArgument()); + } + expect(">"); + result = new ParameterizedTypeImpl(result, typeArguments.toArray(Type[]::new)); + } + if (peekToken().equals("[")) { + return parseArrayType(result); + } + return result; + } else { + throw unexpected(token); + } + } + + private Type parseArrayType(Type component) { + expect("["); + expect("]"); + int dimensions = 1; + while (peekToken().equals("[")) { + expect("["); + expect("]"); + dimensions++; + } + + if (component instanceof Class clazz) { + return parseClassType("[".repeat(dimensions) + (clazz.isPrimitive() ? clazz.descriptorString() : clazz.getName())); + } else { + Type result = component; + for (int i = 0; i < dimensions; i++) { + result = new GenericArrayTypeImpl(result); + } + return result; + } + } + + private Type parseTypeArgument() { + String token = nextToken(); + if (token.equals("?")) { + if (peekToken().equals("extends")) { + expect("extends"); + Type bound = parseReferenceType(nextToken()); + return WildcardTypeImpl.withUpperBound(bound); + } else if (peekToken().equals("super")) { + expect("super"); + Type bound = parseReferenceType(nextToken()); + return WildcardTypeImpl.withLowerBound(bound); + } else { + return WildcardTypeImpl.defaultInstance(); + } + } else { + return parseReferenceType(token); + } + } + + private boolean isPrimitiveType(String token) { + return token.equals("boolean") + || token.equals("byte") + || token.equals("short") + || token.equals("int") + || token.equals("long") + || token.equals("float") + || token.equals("double") + || token.equals("char"); + } + + private Type parsePrimitiveType(String token) { + return switch (token) { + case "boolean" -> boolean.class; + case "byte" -> byte.class; + case "short" -> short.class; + case "int" -> int.class; + case "long" -> long.class; + case "float" -> float.class; + case "double" -> double.class; + case "char" -> char.class; + default -> throw unexpected(token); + }; + } + + private boolean isClassType(String token) { + return !token.isEmpty() && Character.isJavaIdentifierStart(token.charAt(0)); + } + + private Type parseClassType(String token) { + try { + return Class.forName(token, true, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Unknown class: " + token, e); + } + } + + // --- + + private void expect(String expected) { + String token = nextToken(); + if (!expected.equals(token)) { + throw unexpected(token); + } + } + + private IllegalArgumentException unexpected(String token) { + return new IllegalArgumentException("Unexpected token '" + token + "' at position " + (pos - token.length()) + + ": " + str); + } + + private String peekToken() { + // skip whitespace + while (pos < str.length() && Character.isWhitespace(str.charAt(pos))) { + pos++; + } + + // end of input + if (pos == str.length()) { + return ""; + } + + int pos = this.pos; + + // current char is a token on its own + if (isSpecial(str.charAt(pos))) { + return str.substring(pos, pos + 1); + } + + // token is a keyword or fully qualified name + int begin = pos; + while (pos < str.length() && Character.isJavaIdentifierStart(str.charAt(pos))) { + do { + pos++; + } while (pos < str.length() && Character.isJavaIdentifierPart(str.charAt(pos))); + + if (pos < str.length() && str.charAt(pos) == '.') { + pos++; + } else { + return str.substring(begin, pos); + } + } + + if (pos == str.length()) { + throw new IllegalArgumentException("Unexpected end of input: " + str); + } + throw new IllegalArgumentException("Unexpected character '" + str.charAt(pos) + "' at position " + pos + ": " + str); + } + + private String nextToken() { + String result = peekToken(); + pos += result.length(); + return result; + } + + private boolean isSpecial(char c) { + return c == '.' || c == ',' || c == '?' || c == '<' || c == '>' || c == '[' || c == ']'; + } +} diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/impl/GenericArrayTypeImpl.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/impl/GenericArrayTypeImpl.java new file mode 100644 index 0000000000000..1f2f2a70cd9a7 --- /dev/null +++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/impl/GenericArrayTypeImpl.java @@ -0,0 +1,55 @@ +package io.quarkus.cache.redis.runtime.impl; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Type; + +import io.quarkus.arc.impl.ParameterizedTypeImpl; + +/** + * @author Marko Luksa + * @author Jozef Hartinger + */ +public class GenericArrayTypeImpl implements GenericArrayType { + + private Type genericComponentType; + + public GenericArrayTypeImpl(Type genericComponentType) { + this.genericComponentType = genericComponentType; + } + + public GenericArrayTypeImpl(Class rawType, Type... actualTypeArguments) { + this.genericComponentType = new ParameterizedTypeImpl(rawType, actualTypeArguments); + } + + @Override + public Type getGenericComponentType() { + return genericComponentType; + } + + @Override + public int hashCode() { + return ((genericComponentType == null) ? 0 : genericComponentType.hashCode()); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof GenericArrayType) { + GenericArrayType that = (GenericArrayType) obj; + if (genericComponentType == null) { + return that.getGenericComponentType() == null; + } else { + return genericComponentType.equals(that.getGenericComponentType()); + } + } else { + return false; + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(genericComponentType.toString()); + sb.append("[]"); + return sb.toString(); + } +} diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/impl/ParameterizedTypeImpl.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/impl/ParameterizedTypeImpl.java new file mode 100644 index 0000000000000..2990df885c97f --- /dev/null +++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/impl/ParameterizedTypeImpl.java @@ -0,0 +1,86 @@ +package io.quarkus.cache.redis.runtime.impl; + +import java.io.Serializable; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; + +public class ParameterizedTypeImpl implements ParameterizedType, Serializable { + + private static final long serialVersionUID = -3005183010706452884L; + + private final Type[] actualTypeArguments; + private final Type rawType; + private final Type ownerType; + + public ParameterizedTypeImpl(Type rawType, Type... actualTypeArguments) { + this(rawType, actualTypeArguments, null); + } + + public ParameterizedTypeImpl(Type rawType, Type[] actualTypeArguments, Type ownerType) { + this.actualTypeArguments = actualTypeArguments; + this.rawType = rawType; + this.ownerType = ownerType; + } + + @Override + public Type[] getActualTypeArguments() { + return Arrays.copyOf(actualTypeArguments, actualTypeArguments.length); + } + + @Override + public Type getOwnerType() { + return ownerType; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public int hashCode() { + return Arrays.hashCode(actualTypeArguments) ^ (ownerType == null ? 0 : ownerType.hashCode()) + ^ (rawType == null ? 0 : rawType.hashCode()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof ParameterizedType that) { + Type thatOwnerType = that.getOwnerType(); + Type thatRawType = that.getRawType(); + return (ownerType == null ? thatOwnerType == null : ownerType.equals(thatOwnerType)) + && (rawType == null ? thatRawType == null : rawType.equals(thatRawType)) + && Arrays.equals(actualTypeArguments, that.getActualTypeArguments()); + } else { + return false; + } + + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (rawType instanceof Class) { + sb.append(((Class) rawType).getName()); + } else { + sb.append(rawType); + } + if (actualTypeArguments.length > 0) { + sb.append("<"); + for (Type actualType : actualTypeArguments) { + if (actualType instanceof Class) { + sb.append(((Class) actualType).getName()); + } else { + sb.append(actualType); + } + sb.append(", "); + } + sb.delete(sb.length() - 2, sb.length()); + sb.append(">"); + } + return sb.toString(); + } +} diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/impl/WildcardTypeImpl.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/impl/WildcardTypeImpl.java new file mode 100644 index 0000000000000..a971ca1fcbf1a --- /dev/null +++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/impl/WildcardTypeImpl.java @@ -0,0 +1,73 @@ +package io.quarkus.cache.redis.runtime.impl; + +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.Arrays; + +/** + * This code was mainly copied from Weld codebase. + * + * Implementation of {@link WildcardType}. + * + * Note that per JLS a wildcard may define either the upper bound or the lower bound. A wildcard may not have multiple bounds. + * + * @author Jozef Hartinger + * + */ +public class WildcardTypeImpl implements WildcardType { + + public static WildcardType defaultInstance() { + return DEFAULT_INSTANCE; + } + + public static WildcardType withUpperBound(Type type) { + return new WildcardTypeImpl(new Type[] { type }, DEFAULT_LOWER_BOUND); + } + + public static WildcardType withLowerBound(Type type) { + return new WildcardTypeImpl(DEFAULT_UPPER_BOUND, new Type[] { type }); + } + + private static final Type[] DEFAULT_UPPER_BOUND = new Type[] { Object.class }; + private static final Type[] DEFAULT_LOWER_BOUND = new Type[0]; + private static final WildcardType DEFAULT_INSTANCE = new WildcardTypeImpl(DEFAULT_UPPER_BOUND, DEFAULT_LOWER_BOUND); + + private final Type[] upperBound; + private final Type[] lowerBound; + + private WildcardTypeImpl(Type[] upperBound, Type[] lowerBound) { + this.upperBound = upperBound; + this.lowerBound = lowerBound; + } + + @Override + public Type[] getUpperBounds() { + return upperBound; + } + + @Override + public Type[] getLowerBounds() { + return lowerBound; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof WildcardType)) { + return false; + } + WildcardType other = (WildcardType) obj; + return Arrays.equals(lowerBound, other.getLowerBounds()) && Arrays.equals(upperBound, other.getUpperBounds()); + } + + @Override + public int hashCode() { + // We deliberately use the logic from JDK/guava + return Arrays.hashCode(lowerBound) ^ Arrays.hashCode(upperBound); + } +}