Skip to content

Commit

Permalink
feat: add new Shadow variable listener (#954)
Browse files Browse the repository at this point in the history
This pull request introduces a new shadow variable that automatically
updates a list of elements in a planning list variable. It sources
inverse and previous elements shadow variables and cascades updates to
the next elements of a given domain entity.
  • Loading branch information
zepfred authored Jul 23, 2024
1 parent fbae81a commit 10b9258
Show file tree
Hide file tree
Showing 56 changed files with 3,193 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package ai.timefold.solver.core.api.domain.variable;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
* Specifies that field may be updated by the target method when one or more source variables change.
* <p>
* Automatically cascades change events to {@link NextElementShadowVariable} of a {@link PlanningListVariable}.
* <p>
* Important: it must only change the shadow variable(s) for which it's configured.
* It can be applied to multiple fields to modify different shadow variables.
* It should never change a genuine variable or a problem fact.
* It can change its shadow variable(s) on multiple entity instances
* (for example: an arrivalTime change affects all trailing entities too).
*/
@Target({ FIELD })
@Retention(RUNTIME)
@Repeatable(CascadingUpdateShadowVariable.List.class)
public @interface CascadingUpdateShadowVariable {

/**
* The source variable name.
*
* @return never null, a genuine or shadow variable name
*/
String sourceVariableName();

/**
* The target method element.
* <p>
* Important: the method must be non-static and should not include any parameters.
* There are no restrictions regarding the method visibility.
* There is no restriction on the method's return type,
* but if it returns a value, it will be ignored and will not impact the listener's execution.
*
* @return method name of the source host element which will update the shadow variable
*/
String targetMethodName();

/**
* Defines several {@link ShadowVariable} annotations on the same element.
*/
@Target({ FIELD })
@Retention(RUNTIME)
@interface List {

CascadingUpdateShadowVariable[] value();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public static MemberAccessor buildMemberAccessor(Member member, MemberAccessorTy
Class<? extends Annotation> annotationClass, DomainAccessType domainAccessType, ClassLoader classLoader) {
return switch (domainAccessType) {
case GIZMO -> GizmoMemberAccessorFactory.buildGizmoMemberAccessor(member, annotationClass,
memberAccessorType != MemberAccessorType.REGULAR_METHOD,
(GizmoClassLoader) Objects.requireNonNull(classLoader));
case REFLECTION -> buildReflectiveMemberAccessor(member, memberAccessorType, annotationClass);
};
Expand Down Expand Up @@ -82,6 +83,9 @@ private static MemberAccessor buildReflectiveMemberAccessor(Member member, Membe
}
memberAccessor = new ReflectionBeanPropertyMemberAccessor(method, getterOnly);
break;
case REGULAR_METHOD:
memberAccessor = new ReflectionMethodMemberAccessor(method, false);
break;
default:
throw new IllegalStateException("The memberAccessorType (%s) is not implemented."
.formatted(memberAccessorType));
Expand Down Expand Up @@ -163,6 +167,7 @@ public GizmoClassLoader getGizmoClassLoader() {
public enum MemberAccessorType {
FIELD_OR_READ_METHOD,
FIELD_OR_GETTER_METHOD,
FIELD_OR_GETTER_METHOD_WITH_SETTER
FIELD_OR_GETTER_METHOD_WITH_SETTER,
REGULAR_METHOD
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public final class ReflectionMethodMemberAccessor extends AbstractMemberAccessor
private final MethodHandle methodHandle;

public ReflectionMethodMemberAccessor(Method readMethod) {
this(readMethod, true);
}

public ReflectionMethodMemberAccessor(Method readMethod, boolean returnTypeRequired) {
this.readMethod = readMethod;
this.returnType = readMethod.getReturnType();
this.methodName = readMethod.getName();
Expand All @@ -37,7 +41,7 @@ public ReflectionMethodMemberAccessor(Method readMethod) {
throw new IllegalArgumentException("The readMethod (" + readMethod + ") must not have any parameters ("
+ Arrays.toString(readMethod.getParameterTypes()) + ").");
}
if (readMethod.getReturnType() == void.class) {
if (returnTypeRequired && readMethod.getReturnType() == void.class) {
throw new IllegalArgumentException("The readMethod (" + readMethod + ") must have a return type ("
+ readMethod.getReturnType() + ").");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static String getGeneratedClassName(Member member) {
* @return never null
*/
public static MemberAccessor buildGizmoMemberAccessor(Member member, Class<? extends Annotation> annotationClass,
GizmoClassLoader gizmoClassLoader) {
boolean returnTypeRequired, GizmoClassLoader gizmoClassLoader) {
try {
// Check if Gizmo on the classpath by verifying we can access one of its classes
Class.forName("io.quarkus.gizmo.ClassCreator", false,
Expand All @@ -44,7 +44,7 @@ public static MemberAccessor buildGizmoMemberAccessor(Member member, Class<? ext
") the classpath or modulepath must contain io.quarkus.gizmo:gizmo.\n" +
"Maybe add a dependency to io.quarkus.gizmo:gizmo.");
}
return GizmoMemberAccessorImplementor.createAccessorFor(member, annotationClass, gizmoClassLoader);
return GizmoMemberAccessorImplementor.createAccessorFor(member, annotationClass, returnTypeRequired, gizmoClassLoader);
}

private GizmoMemberAccessorFactory() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,26 @@ private static Class<? extends AbstractGizmoMemberAccessor> getCorrectSuperclass
* Creates a MemberAccessor for a given member, generating
* the MemberAccessor bytecode if required
*
* @param member The member to generate a MemberAccessor for.
* @param annotationClass The annotation it was annotated with (used for error reporting), or null.
* @param member The member to generate a MemberAccessor for
* @param annotationClass The annotation it was annotated with (used for
* error reporting)
* @param returnTypeRequired A flag that indicates if the return type is required or optional
* @param gizmoClassLoader never null
* @return A new MemberAccessor that uses Gizmo generated bytecode.
* Will generate the bytecode the first type it is called
* for a member, unless a classloader has been set,
* in which case no Gizmo code will be generated.
*/
static MemberAccessor createAccessorFor(Member member, Class<? extends Annotation> annotationClass,
GizmoClassLoader gizmoClassLoader) {
boolean returnTypeRequired, GizmoClassLoader gizmoClassLoader) {
String className = GizmoMemberAccessorFactory.getGeneratedClassName(member);
if (gizmoClassLoader.hasBytecodeFor(className)) {
return createInstance(className, gizmoClassLoader);
}
final MutableReference<byte[]> classBytecodeHolder = new MutableReference<>(null);
ClassOutput classOutput = (path, byteCode) -> classBytecodeHolder.setValue(byteCode);
GizmoMemberInfo memberInfo = new GizmoMemberInfo(new GizmoMemberDescriptor(member), annotationClass);
GizmoMemberInfo memberInfo =
new GizmoMemberInfo(new GizmoMemberDescriptor(member), returnTypeRequired, annotationClass);
defineAccessorFor(className, classOutput, memberInfo);
byte[] classBytecode = classBytecodeHolder.getValue();

Expand Down Expand Up @@ -160,12 +163,17 @@ private static void createConstructor(ClassCreator classCreator, GizmoMemberInfo
Method.class, String.class, Class[].class),
declaringClass, name,
methodCreator.newArray(Class.class, 0));
ResultHandle type =
methodCreator.invokeVirtualMethod(
MethodDescriptor.ofMethod(Method.class, "getGenericReturnType", Type.class),
method);
methodCreator.writeInstanceField(FieldDescriptor.of(classCreator.getClassName(), GENERIC_TYPE_FIELD, Type.class),
thisObj, type);
if (memberInfo.returnTypeRequired()) {
// We create a field to store the result, only if the called method has a return type.
// Otherwise, we will only execute it
ResultHandle type =
methodCreator.invokeVirtualMethod(
MethodDescriptor.ofMethod(Method.class, "getGenericReturnType", Type.class),
method);
methodCreator.writeInstanceField(
FieldDescriptor.of(classCreator.getClassName(), GENERIC_TYPE_FIELD, Type.class),
thisObj, type);
}
methodCreator.writeInstanceField(
FieldDescriptor.of(classCreator.getClassName(), ANNOTATED_ELEMENT_FIELD, AnnotatedElement.class),
thisObj, method);
Expand Down Expand Up @@ -194,8 +202,9 @@ private static void createGetDeclaringClass(ClassCreator classCreator, GizmoMemb
* Asserts method is a getter or read method
*
* @param method Method to assert is getter or read
* @param returnTypeRequired Flag used to check method return type
*/
private static void assertIsGoodMethod(MethodDescriptor method) {
private static void assertIsGoodMethod(MethodDescriptor method, boolean returnTypeRequired) {
// V = void return type
// Z = primitive boolean return type
String methodName = method.getName();
Expand All @@ -218,7 +227,7 @@ Maybe rename the method (get%s)?"""
}
} else {
// must be a read method
if (method.getReturnType().equals("V")) {
if (returnTypeRequired && method.getReturnType().equals("V")) {
throw new IllegalStateException("The readMethod (%s) must have a non-void return type."
.formatted(methodName));
}
Expand All @@ -229,9 +238,11 @@ Maybe rename the method (get%s)?"""
* Asserts method is a getter or read method
*
* @param method Method to assert is getter or read
* @param returnTypeRequired Flag used to check method return type
* @param annotationClass Used in exception message
*/
private static void assertIsGoodMethod(MethodDescriptor method, Class<? extends Annotation> annotationClass) {
private static void assertIsGoodMethod(MethodDescriptor method, boolean returnTypeRequired,
Class<? extends Annotation> annotationClass) {
// V = void return type
// Z = primitive boolean return type
String methodName = method.getName();
Expand All @@ -256,8 +267,8 @@ Maybe rename the method (get%s)?"""
methodName.substring(2)));
}
} else {
// must be a read method
if (method.getReturnType().equals("V")) {
// must be a read method and return a result only if returnTypeRequired is true
if (returnTypeRequired && method.getReturnType().equals("V")) {
throw new IllegalStateException("The readMethod (%s) with a %s annotation must have a non-void return type."
.formatted(methodName, annotationClass.getSimpleName()));
}
Expand All @@ -279,13 +290,14 @@ Maybe rename the method (get%s)?"""
private static void createGetName(ClassCreator classCreator, GizmoMemberInfo memberInfo) {
MethodCreator methodCreator = getMethodCreator(classCreator, String.class, "getName");

// If it is a method, assert that it has the required properties.
// If it is a method, assert that it has the required
// properties
memberInfo.descriptor().whenIsMethod(method -> {
var annotationClass = memberInfo.annotationClass();
if (annotationClass == null) {
assertIsGoodMethod(method);
assertIsGoodMethod(method, memberInfo.returnTypeRequired());
} else {
assertIsGoodMethod(method, annotationClass);
assertIsGoodMethod(method, memberInfo.returnTypeRequired(), annotationClass);
}
});

Expand Down Expand Up @@ -343,14 +355,23 @@ private static void createGetGenericType(ClassCreator classCreator) {
* }
* </pre>
*
* For a method
* For a method with returning type
*
* <pre>
* Object executeGetter(Object bean) {
* return ((DeclaringClass) bean).method();
* }
* </pre>
*
* For a method without returning type
*
* <pre>
* Object executeGetter(Object bean) {
* ((DeclaringClass) bean).method();
* return null;
* }
* </pre>
*
* The member MUST be public if not called in Quarkus
* (i.e. we don't delegate to the field getter/setter).
* In Quarkus, we generate simple getter/setter for the
Expand All @@ -359,7 +380,13 @@ private static void createGetGenericType(ClassCreator classCreator) {
private static void createExecuteGetter(ClassCreator classCreator, GizmoMemberInfo memberInfo) {
MethodCreator methodCreator = getMethodCreator(classCreator, Object.class, "executeGetter", Object.class);
ResultHandle bean = methodCreator.getMethodParam(0);
methodCreator.returnValue(memberInfo.descriptor().readMemberValue(methodCreator, bean));
if (memberInfo.returnTypeRequired()) {
methodCreator.returnValue(memberInfo.descriptor().readMemberValue(methodCreator, bean));
} else {
memberInfo.descriptor().readMemberValue(methodCreator, bean);
// Returns null as the called method has no return type
methodCreator.returnNull();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

/**
* @param descriptor never null
* @param returnTypeRequired true if the method return type is required
* @param annotationClass null if not annotated
*/
public record GizmoMemberInfo(GizmoMemberDescriptor descriptor, Class<? extends Annotation> annotationClass) {
public record GizmoMemberInfo(GizmoMemberDescriptor descriptor, boolean returnTypeRequired,
Class<? extends Annotation> annotationClass) {

}
Loading

0 comments on commit 10b9258

Please sign in to comment.