Skip to content

Commit

Permalink
Issue #288: fixed handling of attributes defined as type variables (#291
Browse files Browse the repository at this point in the history
)
  • Loading branch information
raner authored May 20, 2024
1 parent a77ea24 commit 7430cb1
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 22 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<bytebuddy.version>1.10.13</bytebuddy.version>
<bytebuddy.version>1.14.12</bytebuddy.version>
</properties>

<scm>
Expand Down
1 change: 1 addition & 0 deletions projo-runtime-code-generation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<version>2.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.AbstractMap.SimpleEntry;
import java.util.HashMap;
Expand Down Expand Up @@ -74,6 +75,7 @@
import pro.projo.internal.rcg.runtime.ToStringObject;
import pro.projo.internal.rcg.runtime.ToStringValueObject;
import pro.projo.internal.rcg.runtime.ValueObject;
import pro.projo.internal.rcg.utilities.GenericTypeResolver;
import pro.projo.internal.rcg.utilities.UncheckedMethodDescription;
import pro.projo.utilities.AnnotationList;
import pro.projo.utilities.MethodInfo;
Expand All @@ -85,8 +87,6 @@
import static java.util.stream.Stream.empty;
import static net.bytebuddy.ClassFileVersion.JAVA_V8;
import static net.bytebuddy.description.modifier.Visibility.PRIVATE;
import static net.bytebuddy.description.type.TypeDescription.OBJECT;
import static net.bytebuddy.description.type.TypeDescription.VOID;
import static net.bytebuddy.dynamic.loading.ClassLoadingStrategy.Default.INJECTION;
import static net.bytebuddy.implementation.bytecode.assign.Assigner.DEFAULT;
import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;
Expand All @@ -102,7 +102,7 @@

/**
* The {@link RuntimeCodeGenerationHandler} is a {@link ProjoHandler} that generates implementation classes
* dynamically at runtime (using the {@link ByteBuddy} library). For each, object property the generated class
* dynamically at runtime (using the {@link ByteBuddy} library). For each object property the generated class
* will contain a field of the appropriate type, and the corresponding generated getter and setter will access
* that field directly, without using reflection. Generated implementation classes can be obtained by calling
* the {@link #getImplementationOf(Class, boolean, ClassLoader)} method.
Expand All @@ -128,6 +128,9 @@ public class RuntimeCodeGenerationHandler<_Artifact_> extends ProjoHandler<_Arti

private final static String SUFFIX = "$Projo";

private final static TypeDescription VOID = TypeDescription.ForLoadedType.of(void.class);
private final static TypeDescription OBJECT = TypeDescription.ForLoadedType.of(Object.class);

private static Map<List<Boolean>, Class<?>> baseClasses = new HashMap<>();

static
Expand Down Expand Up @@ -204,7 +207,7 @@ private Class<? extends _Artifact_> generateImplementation(Class<_Artifact_> typ
Builder<_Artifact_> builder = create(type, additionalImplements, classLoader).name(implementationName(type, defaultPackage));
TypeDescription currentType = builder.make().getTypeDescription();
return debug(getMethods(type, classLoader, additionalImplements, getter, setter, cached, overrides, returns, expects)
.reduce(builder, (accumulator, method) -> add(accumulator, method, additionalImplements, classLoader), sequentialOnly())
.reduce(builder, (accumulator, method) -> add(accumulator, type, method, additionalImplements, classLoader), sequentialOnly())
.defineConstructor(PUBLIC).intercept(constructor(type, currentType, cachedMethods))
.make().load(classLoader(type, defaultPackage, classLoader), INJECTION)).getLoaded();
}
Expand All @@ -226,7 +229,7 @@ private Class<? extends _Artifact_> generateProxy(Class<_Artifact_> type, boolea
.name(implementationName(type, defaultPackage))
.defineField("delegate", delegateType);
builder = additionalAttributes(type, override)
.reduce(builder, (accumulator, method) -> add(accumulator, method, emptyList(), null), sequentialOnly())
.reduce(builder, (accumulator, method) -> add(accumulator, type, method, emptyList(), null), sequentialOnly())
.defineConstructor(Modifier.PUBLIC)
.withParameter(delegateType)
.intercept(MethodCall.invoke(Object.class.getDeclaredConstructor())
Expand Down Expand Up @@ -307,22 +310,25 @@ private Builder<_Artifact_> addProxy(Builder<_Artifact_> builder, Method method,
* Adds method implementation and (if necessary) field definition for a method.
*
* @param builder the existing {@link Builder} to build upon
* @param declaringType the declaring type
* @param method a getter, setter, delegate or cached method
* @param additionalImplements additional interfaces implemented by means of {@link Implements} annotations
* @param classLoader the {@link ClassLoader}
* @return a new {@link Builder} with an additional method (and possibly an additional field)
**/
private Builder<_Artifact_> add(Builder<_Artifact_> builder, Method method, List<String> additionalImplements, ClassLoader classLoader)
private Builder<_Artifact_> add(Builder<_Artifact_> builder, Class<?> declaringType, Method method, List<String> additionalImplements, ClassLoader classLoader)
{
AnnotationList annotations = new AnnotationList(method);
boolean isGetter = getter.test(method) || annotations.contains(Delegate.class) || annotations.contains(Cached.class);
String methodName = annotations.get(Overrides.class).map(Overrides::value).orElse(method.getName());
String propertyName = matcher.propertyName(methodName);
UnaryOperator<Builder<_Artifact_>> addFieldForGetter;
Optional<Annotation> inject = annotations.getInject();
Class<?> returnType = method.getReturnType();
boolean runtimeImplementation = new AnnotationList(declaringType).contains(Implements.class);
Type returnType = GenericTypeResolver.getReturnType(method, runtimeImplementation? method.getDeclaringClass():declaringType);
TypeDescription.Generic type = isGetter? getFieldType(annotations, returnType, classLoader):VOID.asGenericType();
addFieldForGetter = isGetter? localBuilder -> annotate(inject, localBuilder.defineField(propertyName, type, PRIVATE)):identity();
Implementation implementation = getAccessor(method, annotations, returnType, propertyName, additionalImplements, classLoader);
Implementation implementation = getAccessor(method, annotations, declaringType, returnType, propertyName, additionalImplements, classLoader);
Optional<Returns> returns = annotations.get(Returns.class);
Optional<Inherits> inherits = annotations.get(Inherits.class);
List<Optional<Expects>> expects = Stream.of(method.getParameters())
Expand All @@ -335,7 +341,7 @@ private Builder<_Artifact_> add(Builder<_Artifact_> builder, Method method, List
.map(Returns::value)
.map(it -> Projo.forName(it, classLoader))
.map(Class.class::cast)
.orElse(returnType);
.orElse(classOf(returnType));
@SuppressWarnings("rawtypes")
List<Class> parameterTypes = IntStream.range(0, expects.size())
.mapToObj(index -> expects.get(index)
Expand Down Expand Up @@ -365,7 +371,16 @@ private Builder<_Artifact_> add(Builder<_Artifact_> builder, Method method, List
return createMethod.apply(addFieldForGetter.apply(builder)).intercept(implementation);
}

Implementation getAccessor(Method method, AnnotationList annotations, Type returnType, String property, List<String> additionalImplements, ClassLoader classLoader)
Implementation getAccessor
(
Method method,
AnnotationList annotations,
Class<?> declaringType,
Type returnType,
String property,
List<String> additionalImplements,
ClassLoader classLoader
)
{
Optional<Annotation> inject;
if ((inject = annotations.getInject()).isPresent())
Expand Down Expand Up @@ -493,22 +508,22 @@ private ByteBuddy codeGenerator()
* {@code Cache<T>}</li>
* <li>for all other methods returning type {@code T}, the field type is {@code T}</li>
* </ul>
* @param inject an {@link Optional} {@link javax.inject.Inject} annotation
* @param cached an {@link Optional} {@link pro.projo.annotations.Cached} annotation
* @param annotations a list of annotations that are present on the method
* @param originalReturnType the return type of the method
* @param classLoader the {@link ClassLoader}
* @return the appropriate field type for the method
**/
Generic getFieldType(AnnotationList annotations, Class<?> originalReturnType, ClassLoader classLoader)
Generic getFieldType(AnnotationList annotations, Type originalReturnType, ClassLoader classLoader)
{
if (!annotations.containsInject() && !annotations.contains(Cached.class))
{
return Generic.Builder.rawType(originalReturnType).build();
return Generic.Builder.rawType(classOf(originalReturnType)).build();
}
else
{
Optional<Class<?>> optionalContainer = annotations.getInject().map(inject -> Projo.forName(provider(inject), classLoader));
Class<?> container = optionalContainer.orElse(Cache.class);
Type wrappedType = MethodType.methodType(originalReturnType).wrap().returnType();
Type wrappedType = MethodType.methodType(classOf(originalReturnType)).wrap().returnType();
Optional<Returns> returns = annotations.get(Returns.class);
if (returns.isPresent())
{
Expand Down Expand Up @@ -605,6 +620,19 @@ private List<String> getImplements(Class<_Artifact_> type)
return implement.collect(toList());
}

private Class<?> classOf(Type type)
{
if (type instanceof Class)
{
return (Class<?>)type;
}
if (type instanceof ParameterizedType)
{
return (Class<?>)((ParameterizedType)type).getRawType();
}
throw new UnsupportedOperationException(type + " (" + type.getClass() + ")");
}

private TypeDefinition type(Class<?> type)
{
return new TypeDescription.ForLoadedType(type);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// //
// Copyright 2013 - 2024 Peter Walser, Mirko Raner //
// //
// Licensed under the Apache License, Version 2.0 (the "License"); //
// you may not use this file except in compliance with the License. //
// You may obtain a copy of the License at //
// //
// http://www.apache.org/licenses/LICENSE-2.0 //
// //
// Unless required by applicable law or agreed to in writing, software //
// distributed under the License is distributed on an "AS IS" BASIS, //
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
// See the License for the specific language governing permissions and //
// limitations under the License. //
// //
package pro.projo.internal.rcg.utilities;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;

/**
* The {@link GenericTypeResolver} is a utility class whose main purpose is to encapsulate
* Peter Walser's generic type argument resolution code as found in
* <a href="https://stackoverflow.com/questions/17297308#answer-17301917">https://stackoverflow.com/questions/17297308#answer-17301917</a>.
* Per Stack Overflow's <a href="https://stackoverflow.com/legal/terms-of-service/public#licensing">licensing rules</a>,
* section 6, subsection "Subscriber Content", source code snippets posted on the site
* are licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>
* and are hereby further distributed under the Apache License, version 2.0, in a manner
* compatible with CC BY-SA 4.0, including full attribution of authorship. Author and
* copyright holder for the {@link #resolveActualTypeArgs(Class, Class, Type...)}
* method is <a href="https://stackoverflow.com/users/63293">Peter Walser</a>.
*
* @author Peter Walser
* @author Mirko Raner
**/
public class GenericTypeResolver
{
/**
* Gets the actual return type of a method with respect to an actual type, including
* scenarios where the originally declared return type is a type variable.
*
* @param <T> the base type that contains the original method declaration
* @param originalMethodDeclaration the original {@link Method} declaration
* @param actualType the actual concrete type (must be a sub-type of {@code T})
* @return the actual resolved return type (or the bounds type if there was no type variable binding)
**/
public static <T> Type getReturnType(Method originalMethodDeclaration, Class<? extends T> actualType)
{
Type genericReturnType = originalMethodDeclaration.getGenericReturnType();
if (genericReturnType instanceof TypeVariable)
{
String variableName = ((TypeVariable<?>)genericReturnType).getName();
@SuppressWarnings("unchecked")
Class<T> baseClass = (Class<T>)originalMethodDeclaration.getDeclaringClass();
TypeVariable<?>[] typeVariables = baseClass.getTypeParameters();
List<String> variableNames = Stream.of(typeVariables).map(TypeVariable::getName).collect(toList());
int variableIndex = variableNames.indexOf(variableName);
Class<? extends T> offspring = actualType;
Type[] resolvedTypeArguments = resolveActualTypeArgs(offspring, baseClass);
Type resolvedType = resolvedTypeArguments[variableIndex];
if (resolvedType instanceof TypeVariable)
{
return originalMethodDeclaration.getReturnType();
}
else
{
return resolvedType;
}
}
else
{
return genericReturnType;
}
}

/**
* Resolves the actual generic type arguments for a base class, as viewed from a subclass or implementation.
*
* @param <T> base type
* @param offspring class or interface subclassing or extending the base type
* @param base base class
* @param actualArgs the actual type arguments passed to the offspring class
* @return actual generic type arguments, must match the type parameters of the offspring class. If omitted, the
* type parameters will be used instead.
**/
public static <T> Type[] resolveActualTypeArgs (Class<? extends T> offspring, Class<T> base, Type... actualArgs)
{
assert offspring != null;
assert base != null;
assert actualArgs.length == 0 || actualArgs.length == offspring.getTypeParameters().length;

// If actual types are omitted, the type parameters will be used instead.
if (actualArgs.length == 0)
{
actualArgs = offspring.getTypeParameters();
}
// map type parameters into the actual types
Map<String, Type> typeVariables = new HashMap<String, Type>();
for (int i = 0; i < actualArgs.length; i++)
{
TypeVariable<?> typeVariable = (TypeVariable<?>) offspring.getTypeParameters()[i];
typeVariables.put(typeVariable.getName(), actualArgs[i]);
}

// Find direct ancestors (superclass, interfaces)
List<Type> ancestors = new LinkedList<Type>();
if (offspring.getGenericSuperclass() != null)
{
ancestors.add(offspring.getGenericSuperclass());
}
for (Type t: offspring.getGenericInterfaces())
{
ancestors.add(t);
}

// Recurse into ancestors (superclass, interfaces)
for (Type type: ancestors)
{
if (type instanceof Class<?>)
{
// ancestor is non-parameterized. Recurse only if it matches the base class.
Class<?> ancestorClass = (Class<?>) type;
if (base.isAssignableFrom(ancestorClass))
{
@SuppressWarnings("unchecked")
Type[] result = resolveActualTypeArgs((Class<? extends T>) ancestorClass, base);
if (result != null)
{
return result;
}
}
}
if (type instanceof ParameterizedType)
{
// ancestor is parameterized. Recurse only if the raw type matches the base class.
ParameterizedType parameterizedType = (ParameterizedType)type;
Type rawType = parameterizedType.getRawType();
if (rawType instanceof Class<?>)
{
Class<?> rawTypeClass = (Class<?>) rawType;
if (base.isAssignableFrom(rawTypeClass))
{
// loop through all type arguments and replace type variables with the actually known types
List<Type> resolvedTypes = new LinkedList<Type>();
for (Type t: parameterizedType.getActualTypeArguments())
{
if (t instanceof TypeVariable<?>)
{
Type resolvedType = typeVariables.get(((TypeVariable<?>)t).getName());
resolvedTypes.add(resolvedType != null? resolvedType:t);
} else
{
resolvedTypes.add(t);
}
}

@SuppressWarnings("unchecked")
Type[] result = resolveActualTypeArgs((Class<? extends T>) rawTypeClass, base, resolvedTypes.toArray(new Type[] {}));
if (result != null)
{
return result;
}
}
}
}
}

// we have a result if we reached the base class.
return offspring.equals(base) ? actualArgs : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ Bundle-ManifestVersion: 2
Bundle-Name: pro.projo.projo-runtime-code-generation
Bundle-Vendor: Mirko Raner
Bundle-Version: ${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}
Bundle-SymbolicName: pro.projo.projo-runtime-code-generation; singleton:=true
Bundle-ActivationPolicy: lazy
Bundle-SymbolicName: pro.projo.projo-runtime-code-generation; singleton:=true
Automatic-Module-Name: pro.projo.projo-runtime-code-generation
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Fragment-Host: pro.projo.projo
Export-Package: pro.projo.internal.rcg; x-internal:=true,
pro.projo.internal.rcg.runtime; x-internal:=true
Require-Bundle: net.bytebuddy.byte-buddy; bundle-version="1.9.0"
Require-Bundle: net.bytebuddy.byte-buddy; bundle-version="1.14.12"
Loading

0 comments on commit 7430cb1

Please sign in to comment.