From 785ef86a9a774db20f8610a1b73f4d5cdb635eb4 Mon Sep 17 00:00:00 2001 From: Mario Fusco Date: Wed, 15 May 2024 08:57:58 +0200 Subject: [PATCH] [DROOLS-7197] fix generics introspection on superclasses during exec model generation (#5944) --- .../drlxparse/SpecialComparisonCase.java | 13 +- .../expressiontyper/ExpressionTyper.java | 27 +-- .../model/codegen/execmodel/GenericsTest.java | 171 +++++++++++++++++- .../org/drools/mvelcompiler/RHSPhase.java | 12 +- .../ast/FieldToAccessorTExpr.java | 5 + .../mvelcompiler/ast/MethodCallExprT.java | 5 + .../mvelcompiler/ast/TypedExpression.java | 4 + .../mvel/integrationtests/GenericsTest.java | 101 +++++++++++ .../KnownExecModelDifferenceTest.java | 55 ------ .../main/java/org/drools/util/ClassUtils.java | 74 ++++++-- 10 files changed, 355 insertions(+), 112 deletions(-) create mode 100644 drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/GenericsTest.java diff --git a/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/generator/drlxparse/SpecialComparisonCase.java b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/generator/drlxparse/SpecialComparisonCase.java index 7e935b37b12..2378816ac2c 100644 --- a/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/generator/drlxparse/SpecialComparisonCase.java +++ b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/generator/drlxparse/SpecialComparisonCase.java @@ -53,9 +53,7 @@ String getMethodName(BinaryExpr.Operator operator) { static SpecialComparisonCase specialComparisonFactory(TypedExpression left, TypedExpression right) { if (isNumber(left) && !isObject(right.getRawClass()) || isNumber(right) && !isObject(left.getRawClass())) { // Don't coerce Object yet. EvaluationUtil will handle it dynamically later - Optional> leftCast = typeNeedsCast(left.getType()); - Optional> rightCast = typeNeedsCast(right.getType()); - if (leftCast.isPresent() || rightCast.isPresent()) { + if (typeNeedsCast(left.getType()) || typeNeedsCast(right.getType())) { return new ComparisonWithCast(true, left, right, of(Number.class), of(Number.class)); } else { return new NumberComparisonWithoutCast(left, right); @@ -67,13 +65,8 @@ static SpecialComparisonCase specialComparisonFactory(TypedExpression left, Type return new PlainEvaluation(left, right); } - private static Optional> typeNeedsCast(Type t) { - boolean needCast = isObject((Class)t) || isMap((Class) t) || isList((Class) t); - if (needCast) { - return of((Class) t); - } else { - return Optional.empty(); - } + private static boolean typeNeedsCast(Type t) { + return t instanceof Class && ( isObject((Class)t) || isMap((Class) t) || isList((Class) t) ); } private static boolean isList(Class t) { diff --git a/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/generator/expressiontyper/ExpressionTyper.java b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/generator/expressiontyper/ExpressionTyper.java index 928ca3f62ac..6cdc43d6d60 100644 --- a/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/generator/expressiontyper/ExpressionTyper.java +++ b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/generator/expressiontyper/ExpressionTyper.java @@ -22,7 +22,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; -import java.lang.reflect.TypeVariable; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; @@ -94,6 +93,7 @@ import org.drools.mvelcompiler.CompiledExpressionResult; import org.drools.mvelcompiler.ConstraintCompiler; import org.drools.mvelcompiler.util.BigDecimalArgumentCoercion; +import org.drools.util.ClassUtils; import org.drools.util.MethodUtils; import org.drools.util.Pair; import org.drools.util.TypeResolver; @@ -125,6 +125,7 @@ import static org.drools.mvel.parser.MvelParser.parseType; import static org.drools.mvel.parser.printer.PrintUtil.printNode; import static org.drools.util.ClassUtils.extractGenericType; +import static org.drools.util.ClassUtils.actualTypeFromGenerics; import static org.drools.util.ClassUtils.getTypeArgument; import static org.drools.util.ClassUtils.getter2property; import static org.drools.util.ClassUtils.toRawClass; @@ -922,16 +923,7 @@ private TypedExpressionCursor parseMethodCallExpr(MethodCallExpr methodCallExpr, return new TypedExpressionCursor(methodCallExpr, ((ParameterizedType) originalTypeCursor).getActualTypeArguments()[0]); } - java.lang.reflect.Type genericReturnType = m.getGenericReturnType(); - if (genericReturnType instanceof TypeVariable) { - if (originalTypeCursor instanceof ParameterizedType) { - return new TypedExpressionCursor( methodCallExpr, getActualType( rawClassCursor, ( ParameterizedType ) originalTypeCursor, ( TypeVariable ) genericReturnType ) ); - } else { - return new TypedExpressionCursor(methodCallExpr, Object.class); - } - } else { - return new TypedExpressionCursor(methodCallExpr, genericReturnType); - } + return new TypedExpressionCursor(methodCallExpr, actualTypeFromGenerics(originalTypeCursor, m.getGenericReturnType(), rawClassCursor)); } private void promoteBigDecimalParameters(MethodCallExpr methodCallExpr, Class[] argsType, Class[] actualArgumentTypes) { @@ -967,17 +959,6 @@ private Optional checkStartsWithMVEL(MethodCallExpr metho } } - private java.lang.reflect.Type getActualType(Class rawClassCursor, ParameterizedType originalTypeCursor, TypeVariable genericReturnType) { - int genericPos = 0; - for (TypeVariable typeVar : rawClassCursor.getTypeParameters()) { - if (typeVar.equals( genericReturnType )) { - return originalTypeCursor.getActualTypeArguments()[genericPos]; - } - genericPos++; - } - throw new RuntimeException( "Unknonw generic type " + genericReturnType + " for type " + originalTypeCursor ); - } - private TypedExpressionCursor objectCreationExpr(ObjectCreationExpr objectCreationExpr) { parseNodeArguments( objectCreationExpr ); return new TypedExpressionCursor(objectCreationExpr, getClassFromType(ruleContext.getTypeResolver(), objectCreationExpr.getType())); @@ -1163,7 +1144,7 @@ private Optional drlNameExpr(Expression drlxExpr, DrlName return of(new TypedExpressionCursor(addCastToExpression(typeWithoutDollar, fieldAccessor, false), typeOfFirstAccessor ) ); } - return of(new TypedExpressionCursor(fieldAccessor, firstAccessor.getGenericReturnType() ) ); + return of( new TypedExpressionCursor(fieldAccessor, ClassUtils.actualTypeFromGenerics(originalTypeCursor, firstAccessor.getGenericReturnType()) ) ); } Field field = DrlxParseUtil.getField( classCursor, firstName ); diff --git a/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/GenericsTest.java b/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/GenericsTest.java index d6358be006e..40ca7d24f61 100644 --- a/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/GenericsTest.java +++ b/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/GenericsTest.java @@ -20,10 +20,10 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.drools.model.codegen.execmodel.domain.Address; import org.drools.model.codegen.execmodel.domain.Person; - import org.junit.Test; import org.kie.api.runtime.KieSession; @@ -117,4 +117,173 @@ public void testClassWithGenericField() { ksession.insert(classWithGenericField); assertThat(ksession.fireAllRules()).isEqualTo(1); } + + @Test + public void testGenericsOnSuperclass() { + // KIE-DROOLS-5925 + String str = + "import " + DieselCar.class.getCanonicalName() + ";\n " + + "dialect \"mvel\"\n" + + "\n" + + "rule \"Diesel vehicles with more than 95 kW use high-octane fuel (diesel has no octane, this is a test)\"\n" + + " when\n" + + " $v: DieselCar(motor.kw > 95, score<=0, !motor.highOctane)\n" + + " then\n" + + " System.out.println(\"Diesel vehicle with more than 95 kW: \" + $v+\", score=\"+$v.score);\n" + + " $v.engine.highOctane = true;\n" + + " update($v);\n" + + "end\n" + + "\n" + + "rule \"High-octane fuel engines newer serial numbers have slightly higher score\"\n" + + " when\n" + + " $v: DieselCar(engine.highOctane, score<=1, motor.serialNumber > 50000)\n" + + " then\n" + + " System.out.println(\"High octane engine vehicle with newer serial number: \" + $v.motor.serialNumber);\n" + + " $v.score = $v.score + 1;\n" + + " update($v);\n" + + "end"; + + KieSession ksession = getKieSession(str); + + DieselCar vehicle1 = new DieselCar("Volkswagen", "Passat", 100); + vehicle1.setFrameMaxTorque(500); + vehicle1.getEngine().setMaxTorque(350); + vehicle1.getEngine().setSerialNumber(75_000); + vehicle1.setScore(0); + + DieselCar vehicle2 = new DieselCar("Peugeot", "208", 50); + vehicle2.setFrameMaxTorque(100); + vehicle2.getEngine().setMaxTorque(200); + vehicle2.setScore(0); + + ksession.insert(vehicle1); + ksession.insert(vehicle2); + assertThat(ksession.fireAllRules()).isEqualTo(3); + } + + public static abstract class Vehicle { + + private final String maker; + private final String model; + + private int score; + + public Vehicle(String maker, String model) { + this.maker = Objects.requireNonNull(maker); + this.model = Objects.requireNonNull(model); + } + + public String getMaker() { + return maker; + } + + public String getModel() { + return model; + } + + public abstract TEngine getEngine(); + + public TEngine getMotor() { + return getEngine(); + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + + @Override + public String toString() { + return "Vehicle{" + "maker='" + maker + '\'' + ", model='" + model + '\'' + '}'; + } + } + + public static abstract class Engine { + + private final int kw; + + public Engine(int kw) { + this.kw = kw; + } + + public int getKw() { + return kw; + } + + public abstract boolean isZeroEmissions(); + + } + + public static class DieselEngine extends Engine { + + // diesel has no octanes... but let's pretend it does + private boolean highOctane; + + private int maxTorque; + + private long serialNumber; + + public DieselEngine(int kw) { + super(kw); + } + + @Override + public boolean isZeroEmissions() { + return false; + } + + public boolean isHighOctane() { + return highOctane; + } + + public void setHighOctane(boolean highOctane) { + this.highOctane = highOctane; + } + + public int getMaxTorque() { + return maxTorque; + } + + public void setMaxTorque(int maxTorque) { + this.maxTorque = maxTorque; + } + + public void setSerialNumber(long serialNumber) { + this.serialNumber = serialNumber; + } + + public long getSerialNumber() { + return serialNumber; + } + + } + + public static class DieselCar extends Vehicle { + private final DieselEngine engine; + + private long frameMaxTorque; + + + + public DieselCar(String maker, String model, int kw) { + super(maker, model); + this.engine = new DieselEngine(kw); + } + + @Override + public DieselEngine getEngine() { + return engine; + } + + public long getFrameMaxTorque() { + return frameMaxTorque; + } + + public void setFrameMaxTorque(long frameMaxTorque) { + this.frameMaxTorque = frameMaxTorque; + } + } } \ No newline at end of file diff --git a/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/RHSPhase.java b/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/RHSPhase.java index 6751147ef9e..ef026350544 100644 --- a/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/RHSPhase.java +++ b/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/RHSPhase.java @@ -216,12 +216,14 @@ private Optional asEnum(SimpleName n) { private Optional asPropertyAccessor(SimpleName n, VisitorContext arg) { Optional lastTypedExpression = arg.getScope(); - Optional scopeType = lastTypedExpression.filter(ListAccessExprT.class::isInstance) - .map(ListAccessExprT.class::cast) - .map(expr -> expr.getElementType()) - .orElse(arg.getScopeType()); + Optional propertyType = lastTypedExpression.filter(ListAccessExprT.class::isInstance) + .map(ListAccessExprT.class::cast) + .map(expr -> expr.getElementType()) + .orElse(arg.getScopeType()); - Optional optAccessor = scopeType.flatMap(t -> ofNullable(getAccessor(classFromType(t), n.asString()))); + Optional scopeType = lastTypedExpression.flatMap(TypedExpression::getScopeType); + + Optional optAccessor = propertyType.flatMap(t -> ofNullable(getAccessor(classFromType(t, scopeType.orElse(null)), n.asString()))); return map2(lastTypedExpression, optAccessor, FieldToAccessorTExpr::new); } diff --git a/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/ast/FieldToAccessorTExpr.java b/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/ast/FieldToAccessorTExpr.java index fb4047e7804..e96e3143890 100644 --- a/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/ast/FieldToAccessorTExpr.java +++ b/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/ast/FieldToAccessorTExpr.java @@ -68,6 +68,11 @@ public Optional getType() { return Optional.of(type); } + @Override + public Optional getScopeType() { + return scope.getType(); + } + @Override public Node toJavaExpression() { List expressionArguments = this.arguments.stream() diff --git a/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/ast/MethodCallExprT.java b/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/ast/MethodCallExprT.java index 5d3337edc50..d2d93edce41 100644 --- a/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/ast/MethodCallExprT.java +++ b/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/ast/MethodCallExprT.java @@ -57,6 +57,11 @@ public Optional getType() { return type; } + @Override + public Optional getScopeType() { + return scope.flatMap(TypedExpression::getScopeType); + } + @Override public Node toJavaExpression() { Node scopeE = scope.map(TypedExpression::toJavaExpression).orElse(null); diff --git a/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/ast/TypedExpression.java b/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/ast/TypedExpression.java index d83d9a2a078..9f4ee5dd767 100644 --- a/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/ast/TypedExpression.java +++ b/drools-model/drools-mvel-compiler/src/main/java/org/drools/mvelcompiler/ast/TypedExpression.java @@ -28,5 +28,9 @@ public interface TypedExpression { Optional getType(); Node toJavaExpression(); + + default Optional getScopeType() { + return Optional.empty(); + } } diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/GenericsTest.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/GenericsTest.java new file mode 100644 index 00000000000..cb5a94aa2cf --- /dev/null +++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/GenericsTest.java @@ -0,0 +1,101 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.drools.mvel.integrationtests; + +import java.util.Collection; + +import org.drools.mvel.integrationtests.facts.vehicles.DieselCar; +import org.drools.mvel.integrationtests.facts.vehicles.ElectricCar; +import org.drools.testcoverage.common.util.KieBaseTestConfiguration; +import org.drools.testcoverage.common.util.KieBaseUtil; +import org.drools.testcoverage.common.util.TestParametersUtil; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.kie.api.KieBase; +import org.kie.api.runtime.KieSession; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This is a place where known behavior differences between exec-model and non-exec-model. + * They are not treated as a bug and should be documented in "Migration from non-executable model to executable model" section + */ +@RunWith(Parameterized.class) +public class GenericsTest { + + private final KieBaseTestConfiguration kieBaseTestConfiguration; + + public GenericsTest(final KieBaseTestConfiguration kieBaseTestConfiguration) { + this.kieBaseTestConfiguration = kieBaseTestConfiguration; + } + + @Parameterized.Parameters(name = "KieBase type={0}") + public static Collection getParameters() { + return TestParametersUtil.getKieBaseCloudConfigurations(true); + } + + @Test + public void property_subClassMethod_genericsReturnType() { + // DROOLS-7197 + String str = "package com.example.reproducer\n" + + "import " + DieselCar.class.getCanonicalName() + ";\n" + + "rule R\n" + + "dialect \"mvel\"\n" + + "when\n" + + " $v : DieselCar(motor.adBlueRequired == true)\n" + + "then\n" + + " $v.score = 5;\n" + + "end"; + + KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl("test", kieBaseTestConfiguration, str); + KieSession ksession = kbase.newKieSession(); + + DieselCar dieselCar = new DieselCar("ABC", "Model 1.6", 85, true); + + ksession.insert(dieselCar); + ksession.fireAllRules(); + + assertThat(dieselCar.getScore()).isEqualTo(5); + } + + @Test + public void property_subClassMethod_explicitReturnType() { + // DROOLS-7197 + String str = "package com.example.reproducer\n" + + "import " + ElectricCar.class.getCanonicalName() + ";\n" + + "rule R\n" + + "dialect \"mvel\"\n" + + "when\n" + + " $v : ElectricCar(engine.batterySize > 70)\n" + + "then\n" + + " $v.score = 5;\n" + + "end"; + + KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl("test", kieBaseTestConfiguration, str); + KieSession ksession = kbase.newKieSession(); + + ElectricCar electricCar = new ElectricCar("XYZ", "Model 3", 200, 90); + + ksession.insert(electricCar); + ksession.fireAllRules(); + + assertThat(electricCar.getScore()).isEqualTo(5); // works for both cases + } +} diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/KnownExecModelDifferenceTest.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/KnownExecModelDifferenceTest.java index 5282f09451c..f42c3ab4cce 100644 --- a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/KnownExecModelDifferenceTest.java +++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/KnownExecModelDifferenceTest.java @@ -25,8 +25,6 @@ import org.drools.mvel.compiler.Person; import org.drools.mvel.integrationtests.facts.VarargsFact; -import org.drools.mvel.integrationtests.facts.vehicles.DieselCar; -import org.drools.mvel.integrationtests.facts.vehicles.ElectricCar; import org.drools.testcoverage.common.util.KieBaseTestConfiguration; import org.drools.testcoverage.common.util.KieBaseUtil; import org.drools.testcoverage.common.util.KieUtil; @@ -164,59 +162,6 @@ public void setter_intToPrimitiveLongCoercionVarargs() { assertThat(fact.getValueList()).containsExactly(10L, 20L); // Coerced with both cases } - @Test - public void property_subClassMethod_genericsReturnType() { - // DROOLS-7197 - String str = "package com.example.reproducer\n" + - "import " + DieselCar.class.getCanonicalName() + ";\n" + - "rule R\n" + - "dialect \"mvel\"\n" + - "when\n" + - " $v : DieselCar(motor.adBlueRequired == true)\n" + - "then\n" + - " $v.score = 5;\n" + - "end"; - - if (kieBaseTestConfiguration.isExecutableModel()) { - KieBuilder kieBuilder = KieUtil.getKieBuilderFromDrls(kieBaseTestConfiguration, false, str); - assertThat(kieBuilder.getResults().hasMessages(Level.ERROR)).isTrue(); // Fail with exec-model - } else { - KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl("test", kieBaseTestConfiguration, str); - KieSession ksession = kbase.newKieSession(); - - DieselCar dieselCar = new DieselCar("ABC", "Model 1.6", 85, true); - - ksession.insert(dieselCar); - ksession.fireAllRules(); - - assertThat(dieselCar.getScore()).isEqualTo(5); - } - } - - @Test - public void property_subClassMethod_explicitReturnType() { - // DROOLS-7197 - String str = "package com.example.reproducer\n" + - "import " + ElectricCar.class.getCanonicalName() + ";\n" + - "rule R\n" + - "dialect \"mvel\"\n" + - "when\n" + - " $v : ElectricCar(engine.batterySize > 70)\n" + - "then\n" + - " $v.score = 5;\n" + - "end"; - - KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl("test", kieBaseTestConfiguration, str); - KieSession ksession = kbase.newKieSession(); - - ElectricCar electricCar = new ElectricCar("XYZ", "Model 3", 200, 90); - - ksession.insert(electricCar); - ksession.fireAllRules(); - - assertThat(electricCar.getScore()).isEqualTo(5); // works for both cases - } - @Test public void invalid_cast_intToString() { // DROOLS-7198 diff --git a/drools-util/src/main/java/org/drools/util/ClassUtils.java b/drools-util/src/main/java/org/drools/util/ClassUtils.java index 90ac3252286..b9db8dd786f 100644 --- a/drools-util/src/main/java/org/drools/util/ClassUtils.java +++ b/drools-util/src/main/java/org/drools/util/ClassUtils.java @@ -104,8 +104,8 @@ public static Class toNonPrimitiveType(Class c) { if (c == boolean.class) return Boolean.class; return c; } - - + + /** * Please do not use - internal * org/my/Class.xxx -> org.my.Class @@ -315,11 +315,11 @@ public static boolean isMatched(Map patterns, // see http://download.oracle.com/javase/6/docs/api/java/lang/Class.html#getName%28%29 String qualifiedNamespace = className; String name = className; - if( className.indexOf('.') > 0 ) { + if( className.indexOf('.') > 0 ) { qualifiedNamespace = className.substring( 0, className.lastIndexOf( '.' ) ).trim(); name = className.substring( className.lastIndexOf( '.' ) + 1 ).trim(); } - else if( className.indexOf('[') == 0 ) { + else if( className.indexOf('[') == 0 ) { qualifiedNamespace = className.substring(0, className.lastIndexOf('[') ); } Object object = patterns.get( qualifiedNamespace ); @@ -351,7 +351,7 @@ public static String getPackage(Class cls) { } else { dotPos = cls.getName().lastIndexOf( '.' ); } - + if ( dotPos > 0 ) { return cls.getName().substring( 0, dotPos ); @@ -556,17 +556,17 @@ public static Class extractGenericType(Class clazz, final String methodName) } throw new RuntimeException("No generic type"); } - + public static Type getTypeArgument(Type genericType, int index) { return genericType instanceof ParameterizedType ? (( ParameterizedType ) genericType).getActualTypeArguments()[index] : Object.class; } - + public static boolean isAssignableFrom(Type from, Type to) { Class fromClass = toRawClass( from ); Class toClass = toRawClass( to ); return fromClass.isAssignableFrom(toClass) || MethodUtils.areBoxingCompatible(fromClass, toClass); - } + } public static boolean isCollection(Type t) { @@ -577,15 +577,20 @@ public static boolean isCollection(Type t) { } public static Class classFromType(Type t) { - Class clazz; - if(t instanceof Class) { - clazz = (Class) t; - } else if(t instanceof ParameterizedType) { - clazz = (Class) ((ParameterizedType)t).getRawType(); - } else { - throw new UnsupportedOperationException("Unable to parse type"); + return classFromType(t, null); + } + + public static Class classFromType(Type t, Type scope) { + if (t instanceof Class) { + return (Class) t; } - return clazz; + if (t instanceof ParameterizedType) { + return (Class) ((ParameterizedType)t).getRawType(); + } + if (t instanceof TypeVariable && scope != null) { + return classFromType( actualTypeFromGenerics(scope, t) ); + } + throw new UnsupportedOperationException("Unable to parse type"); } public static Class toRawClass(Type type) { @@ -603,7 +608,7 @@ public static Class toRawClass(Type type) { } throw new UnsupportedOperationException( "Unknown type " + type ); } - + public static Class rawType(Type type) { if (type == null) { return null; @@ -670,7 +675,7 @@ public static boolean isConvertible( Class srcPrimitive, Class tgtPrimitiv public static boolean isFinal(Class clazz) { return Modifier.isFinal( clazz.getModifiers() ); } - + public static boolean isInterface(Class clazz) { return Modifier.isInterface( clazz.getModifiers() ); } @@ -914,4 +919,37 @@ public static ClassLoader findParentClassLoader(Class invokingClass) { public static boolean isNumericClass(Class clazz) { return numericClasses.contains(clazz); } + + public static Type actualTypeFromGenerics(Type scope, Type genericType) { + return actualTypeFromGenerics(scope, genericType, toRawClass(scope)); + } + + public static Type actualTypeFromGenerics(Type scope, Type genericType, Class rawClassCursor) { + if (genericType instanceof Class || genericType instanceof ParameterizedType) { + return genericType; + } + if (genericType instanceof TypeVariable typeVar) { + if (scope instanceof ParameterizedType paramType) { + return actualTypeFromGenerics(rawClassCursor, paramType, typeVar); + } + if (scope instanceof Class classCursor && classCursor.getSuperclass() != null && classCursor.getSuperclass() != Object.class) { + return actualTypeFromGenerics(classCursor.getGenericSuperclass(), genericType, classCursor.getSuperclass()); + } + if (typeVar.getBounds().length == 1 && typeVar.getBounds()[0] instanceof Class) { + return typeVar.getBounds()[0]; + } + } + return Object.class; + } + + private static Type actualTypeFromGenerics(Class rawClassCursor, ParameterizedType originalTypeCursor, TypeVariable genericType) { + int genericPos = 0; + for (TypeVariable typeVar : rawClassCursor.getTypeParameters()) { + if (typeVar.equals( genericType )) { + return originalTypeCursor.getActualTypeArguments()[genericPos]; + } + genericPos++; + } + throw new RuntimeException( "Unknonw generic type " + genericType + " for type " + originalTypeCursor ); + } }