Skip to content

Commit

Permalink
Redis Cache: support more complex types
Browse files Browse the repository at this point in the history
This includes generic types, like `List<String>`, array types,
like `int[]`, and more.

The type parser in this commit is reasonably complete. Omitting
type variables should not be an issue; nested types might, but
adding support for them should be doable later.
  • Loading branch information
Ladicek committed Sep 18, 2024
1 parent 3094004 commit bcc9bb7
Show file tree
Hide file tree
Showing 15 changed files with 1,021 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.WildcardType;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
Expand Down Expand Up @@ -71,6 +73,9 @@
import io.quarkus.runtime.StartupTask;
import io.quarkus.runtime.annotations.IgnoreProperty;
import io.quarkus.runtime.annotations.RelaxedValidation;
import io.quarkus.runtime.types.GenericArrayTypeImpl;
import io.quarkus.runtime.types.ParameterizedTypeImpl;
import io.quarkus.runtime.types.WildcardTypeImpl;

/**
* A class that can be used to record invocations to bytecode so they can be replayed later. This is done through the
Expand Down Expand Up @@ -769,6 +774,68 @@ ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle ar
}
};
}
} else if (param instanceof ParameterizedType parameterized) {
DeferredParameter raw = loadObjectInstance(parameterized.getRawType(), existing,
java.lang.reflect.Type.class, relaxedValidation);
DeferredParameter args = loadObjectInstance(parameterized.getActualTypeArguments(), existing,
java.lang.reflect.Type[].class, relaxedValidation);
DeferredParameter owner = loadObjectInstance(parameterized.getOwnerType(), existing,
java.lang.reflect.Type.class, relaxedValidation);
return new DeferredParameter() {
@Override
ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) {
return method.newInstance(ofConstructor(ParameterizedTypeImpl.class, java.lang.reflect.Type.class,
java.lang.reflect.Type[].class, java.lang.reflect.Type.class),
context.loadDeferred(raw), context.loadDeferred(args), context.loadDeferred(owner));
}
};
} else if (param instanceof GenericArrayType array) {
DeferredParameter res = loadObjectInstance(array.getGenericComponentType(), existing,
java.lang.reflect.Type.class, relaxedValidation);
return new DeferredParameter() {
@Override
ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) {
return method.newInstance(ofConstructor(GenericArrayTypeImpl.class, java.lang.reflect.Type.class),
context.loadDeferred(res));
}
};
} else if (param instanceof WildcardType wildcard) {
java.lang.reflect.Type[] upperBound = wildcard.getUpperBounds();
java.lang.reflect.Type[] lowerBound = wildcard.getLowerBounds();
if (lowerBound.length == 0 && upperBound.length == 1 && Object.class.equals(upperBound[0])) {
// unbounded
return new DeferredParameter() {
@Override
ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) {
return method.invokeStaticMethod(ofMethod(WildcardTypeImpl.class, "defaultInstance",
WildcardType.class));
}
};
} else if (lowerBound.length == 0 && upperBound.length == 1) {
// upper bound
DeferredParameter res = loadObjectInstance(upperBound[0], existing,
java.lang.reflect.Type.class, relaxedValidation);
return new DeferredParameter() {
@Override
ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) {
return method.invokeStaticMethod(ofMethod(WildcardTypeImpl.class, "withUpperBound",
WildcardType.class, java.lang.reflect.Type.class), context.loadDeferred(res));
}
};
} else if (lowerBound.length == 1) {
// lower bound
DeferredParameter res = loadObjectInstance(lowerBound[0], existing,
java.lang.reflect.Type.class, relaxedValidation);
return new DeferredParameter() {
@Override
ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) {
return method.invokeStaticMethod(ofMethod(WildcardTypeImpl.class, "withLowerBound",
WildcardType.class, java.lang.reflect.Type.class), context.loadDeferred(res));
}
};
} else {
throw new UnsupportedOperationException("Unsupported wildcard type: " + wildcard);
}
} else if (expectedType == boolean.class || expectedType == Boolean.class || param instanceof Boolean) {
return new DeferredParameter() {
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package io.quarkus.deployment.types;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import io.quarkus.runtime.types.GenericArrayTypeImpl;
import io.quarkus.runtime.types.ParameterizedTypeImpl;
import io.quarkus.runtime.types.WildcardTypeImpl;

/**
* Creates a {@link Type} by parsing the given string according to the following grammar:
*
* <pre>
* 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
* </pre>
*
* 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.
* <p>
* Types produced by this parser can be transferred from build time to runtime
* via the recorder mechanism.
*/
public class TypeParser {
public static Type parse(String str) {
return new TypeParser(str).parse();
}

private final String str;

private int pos = 0;

private TypeParser(String str) {
this.str = Objects.requireNonNull(str);
}

private 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<Type> 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 elementType) {
expect("[");
expect("]");
int dimensions = 1;
while (peekToken().equals("[")) {
expect("[");
expect("]");
dimensions++;
}

if (elementType instanceof Class<?> clazz) {
return parseClassType("[".repeat(dimensions)
+ (clazz.isPrimitive() ? clazz.descriptorString() : "L" + clazz.getName() + ";"));
} else {
Type result = elementType;
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) {
if (token.isEmpty()) {
throw new IllegalArgumentException("Unexpected end of input: " + str);
}
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 == ']';
}
}
Loading

0 comments on commit bcc9bb7

Please sign in to comment.