diff --git a/pom.xml b/pom.xml index 453c118..0659ff9 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ UTF-8 - 1.10.13 + 1.14.12 diff --git a/projo-runtime-code-generation/pom.xml b/projo-runtime-code-generation/pom.xml index 098564d..d376ed1 100644 --- a/projo-runtime-code-generation/pom.xml +++ b/projo-runtime-code-generation/pom.xml @@ -57,6 +57,7 @@ jakarta.inject jakarta.inject-api 2.0.1 + test com.google.inject diff --git a/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/RuntimeCodeGenerationHandler.java b/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/RuntimeCodeGenerationHandler.java index 8d2973f..9e07635 100644 --- a/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/RuntimeCodeGenerationHandler.java +++ b/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/RuntimeCodeGenerationHandler.java @@ -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; @@ -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; @@ -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; @@ -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. @@ -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, Class> baseClasses = new HashMap<>(); static @@ -204,7 +207,7 @@ private Class 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(); } @@ -226,7 +229,7 @@ private Class 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()) @@ -307,11 +310,13 @@ 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 additionalImplements, ClassLoader classLoader) + private Builder<_Artifact_> add(Builder<_Artifact_> builder, Class declaringType, Method method, List additionalImplements, ClassLoader classLoader) { AnnotationList annotations = new AnnotationList(method); boolean isGetter = getter.test(method) || annotations.contains(Delegate.class) || annotations.contains(Cached.class); @@ -319,10 +324,11 @@ private Builder<_Artifact_> add(Builder<_Artifact_> builder, Method method, List String propertyName = matcher.propertyName(methodName); UnaryOperator> addFieldForGetter; Optional 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 = annotations.get(Returns.class); Optional inherits = annotations.get(Inherits.class); List> expects = Stream.of(method.getParameters()) @@ -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 parameterTypes = IntStream.range(0, expects.size()) .mapToObj(index -> expects.get(index) @@ -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 additionalImplements, ClassLoader classLoader) + Implementation getAccessor + ( + Method method, + AnnotationList annotations, + Class declaringType, + Type returnType, + String property, + List additionalImplements, + ClassLoader classLoader + ) { Optional inject; if ((inject = annotations.getInject()).isPresent()) @@ -493,22 +508,22 @@ private ByteBuddy codeGenerator() * {@code Cache} *
  • for all other methods returning type {@code T}, the field type is {@code T}
  • * - * @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> 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 = annotations.get(Returns.class); if (returns.isPresent()) { @@ -605,6 +620,19 @@ private List 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); diff --git a/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/utilities/GenericTypeResolver.java b/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/utilities/GenericTypeResolver.java new file mode 100644 index 0000000..e744c70 --- /dev/null +++ b/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/utilities/GenericTypeResolver.java @@ -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 +* https://stackoverflow.com/questions/17297308#answer-17301917. +* Per Stack Overflow's licensing rules, +* section 6, subsection "Subscriber Content", source code snippets posted on the site +* are licensed under CC BY-SA 4.0 +* 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 Peter Walser. +* +* @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 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 Type getReturnType(Method originalMethodDeclaration, Class actualType) + { + Type genericReturnType = originalMethodDeclaration.getGenericReturnType(); + if (genericReturnType instanceof TypeVariable) + { + String variableName = ((TypeVariable)genericReturnType).getName(); + @SuppressWarnings("unchecked") + Class baseClass = (Class)originalMethodDeclaration.getDeclaringClass(); + TypeVariable[] typeVariables = baseClass.getTypeParameters(); + List variableNames = Stream.of(typeVariables).map(TypeVariable::getName).collect(toList()); + int variableIndex = variableNames.indexOf(variableName); + Class 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 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 Type[] resolveActualTypeArgs (Class offspring, Class 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 typeVariables = new HashMap(); + 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 ancestors = new LinkedList(); + 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) 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 resolvedTypes = new LinkedList(); + 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) 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; + } +} diff --git a/projo-runtime-code-generation/src/main/resources/META-INF/MANIFEST.MF b/projo-runtime-code-generation/src/main/resources/META-INF/MANIFEST.MF index 655280d..bd1075a 100644 --- a/projo-runtime-code-generation/src/main/resources/META-INF/MANIFEST.MF +++ b/projo-runtime-code-generation/src/main/resources/META-INF/MANIFEST.MF @@ -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" diff --git a/projo-runtime-code-generation/src/test/java/pro/projo/integration/tests/ProjoRCGManifestIT.java b/projo-runtime-code-generation/src/test/java/pro/projo/integration/tests/ProjoRCGManifestIT.java index 9a810a0..b3745d6 100644 --- a/projo-runtime-code-generation/src/test/java/pro/projo/integration/tests/ProjoRCGManifestIT.java +++ b/projo-runtime-code-generation/src/test/java/pro/projo/integration/tests/ProjoRCGManifestIT.java @@ -1,5 +1,5 @@ // // -// Copyright 2022 Mirko Raner // +// Copyright 2022 - 2024 Mirko Raner // // // // Licensed under the Apache License, Version 2.0 (the "License"); // // you may not use this file except in compliance with the License. // @@ -79,7 +79,7 @@ public String expectedFragmentHost() @Override public Set expectedRequireBundle() { - return Collections.singleton("net.bytebuddy.byte-buddy; bundle-version=\"1.9.0\""); + return Collections.singleton("net.bytebuddy.byte-buddy; bundle-version=\"1.14.12\""); } @Test diff --git a/projo/src/main/java/pro/projo/internal/Numbers.java b/projo/src/main/java/pro/projo/internal/Numbers.java index 42ee283..52e7734 100644 --- a/projo/src/main/java/pro/projo/internal/Numbers.java +++ b/projo/src/main/java/pro/projo/internal/Numbers.java @@ -1,5 +1,5 @@ // // -// Copyright 2019 Mirko Raner // +// Copyright 2019 - 2024 Mirko Raner // // // // Licensed under the Apache License, Version 2.0 (the "License"); // // you may not use this file except in compliance with the License. // @@ -172,6 +172,12 @@ <_Numeric_ extends Number> _Numeric_ to(Class<_Numeric_> type) Function function = map.get(new SimpleEntry<>(number.getClass(), type)); if (function == null) { + if (Number.class.equals(type)) + { + @SuppressWarnings("unchecked") + _Numeric_ numeric = (_Numeric_)number; + return numeric; + } throw new IllegalArgumentException(number.getClass() + " cannot be cast to " + type); } @SuppressWarnings("unchecked") diff --git a/projo/src/test/java/pro/projo/BuilderTest.java b/projo/src/test/java/pro/projo/BuilderTest.java index cf8d935..33338b9 100644 --- a/projo/src/test/java/pro/projo/BuilderTest.java +++ b/projo/src/test/java/pro/projo/BuilderTest.java @@ -1,5 +1,5 @@ // // -// Copyright 2019 - 2021 Mirko Raner // +// Copyright 2019 - 2024 Mirko Raner // // // // Licensed under the Apache License, Version 2.0 (the "License"); // // you may not use this file except in compliance with the License. // @@ -16,7 +16,10 @@ package pro.projo; import org.junit.Test; +import java.util.AbstractMap.SimpleEntry; +import java.util.Map.Entry; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; public class BuilderTest @@ -46,6 +49,33 @@ static interface LongInterval long end(); } + static interface Quantity + { + VALUE value(); + } + + static interface RealQuantity extends Quantity + { + // No additional methods + } + + static interface StringBoolean extends Entry + { + // No additional methods + } + + @SuppressWarnings("rawtypes") + static interface Something<$ extends Something> {} + + @SuppressWarnings("rawtypes") + static interface SIQuantity<$ extends SIQuantity, VALUE extends Number> extends Something<$> + { + VALUE value(); + } + + @SuppressWarnings("rawtypes") + static interface RealSIQuantity<$ extends RealSIQuantity> extends SIQuantity<$, Double> {} + @Test public void testBuilderWithTwoProperties() { @@ -158,4 +188,60 @@ public void testBuilderWithIncorrectPropertyTypeAndLossyCast() IllegalArgumentException.class, () -> Projo.builder(Interval.class).with(Interval::begin, -1).with(Interval::end, 1L).build()); } + + @Test + public void testRealQuantityWithGenericType() + { + Number fortyTwo = Double.valueOf(42D); + RealQuantity quantity = Projo.builder(RealQuantity.class).with(Quantity::value, fortyTwo).build(); + assertEquals(Double.valueOf(42D), quantity.value()); + } + + @Test + public void testRealQuantityWithGenericTypeButSpecificValue() + { + RealQuantity quantity = Projo.builder(RealQuantity.class).with(Quantity::value, Double.valueOf(42D)).build(); + assertEquals(Double.valueOf(42D), quantity.value()); + } + + @Test + public void testRealQuantityWithSpecificType() + { + RealQuantity quantity = Projo.builder(RealQuantity.class).with(RealQuantity::value, Double.valueOf(42D)).build(); + assertEquals(Double.valueOf(42D), quantity.value()); + } + + @Test + public void testRealSIQuantityWithGenericTypeButSpecificValue() + { + RealSIQuantity quantity = Projo.builder(RealSIQuantity.class).with(SIQuantity::value, Double.valueOf(42D)).build(); + assertEquals(Double.valueOf(42D), quantity.value()); + } + + @Test + public void testRealSIQuantityWithSpecificType() + { + RealSIQuantity quantity = Projo.builder(RealSIQuantity.class).with(RealSIQuantity::value, Double.valueOf(42D)).build(); + assertEquals(Double.valueOf(42D), quantity.value()); + } + + @Test + public void testMapEntry() + { + StringBoolean pair = Projo.builder(StringBoolean.class) + .with(StringBoolean::getKey, "answer") + .with(StringBoolean::getValue, true) + .build(); + assertEquals(new SimpleEntry("answer", true), pair); + } + + @Test + public void testMapEntryWithGenericMethods() + { + StringBoolean pair = Projo.builder(StringBoolean.class) + .with(Entry::getKey, "answer") + .with(Entry::getValue, true) + .build(); + assertEquals(new SimpleEntry("answer", true), pair); + } }