From aa53c8eb81836e4cd98a2c6220e4297ac28ba0a0 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Fri, 13 Feb 2026 16:08:11 -0500 Subject: [PATCH 1/3] chore: use Gizmo MemberAccessors when possible, remove domainAccessType from SolverConfig Note: SolutionCloner will still use REFLECTION by default, since GIZMO cannot copy final fields and requires all fields to be accessible (i.e. either public or have public getters and setters). --- core/pom.xml | 1 - .../core/config/solver/SolverConfig.java | 21 -- .../solver/core/config/util/ConfigUtils.java | 2 +- .../domain/common/DomainAccessType.java | 19 +- .../accessor/MemberAccessorFactory.java | 52 ++- .../ReflectionBeanPropertyMemberAccessor.java | 4 +- .../common/accessor/gizmo/AccessorInfo.java | 15 +- .../accessor/gizmo/GizmoClassLoader.java | 10 + .../accessor/gizmo/GizmoFieldHandler.java | 14 +- .../gizmo/GizmoMemberAccessorFactory.java | 64 +++- .../gizmo/GizmoMemberAccessorImplementor.java | 113 ++++--- .../accessor/gizmo/GizmoMemberDescriptor.java | 26 +- .../accessor/gizmo/GizmoSupportStatus.java | 7 + .../domain/lookup/LookUpStrategyResolver.java | 2 +- .../impl/domain/policy/DescriptorPolicy.java | 2 +- .../solution/ConstraintWeightSupplier.java | 2 +- ...verridesBasedConstraintWeightSupplier.java | 2 +- .../gizmo/GizmoSolutionClonerFactory.java | 9 - .../descriptor/SolutionDescriptor.java | 18 +- .../impl/solver/DefaultSolverFactory.java | 3 +- core/src/main/resources/solver.xsd | 14 - .../core/config/solver/SolverConfigTest.java | 6 +- .../accessor/MemberAccessorFactoryTest.java | 2 +- ...lectionBeanPropertyMemberAccessorTest.java | 36 ++- ...ctionMethodExtendedMemberAccessorTest.java | 55 ++-- .../gizmo/GizmoMemberAccessorFactoryTest.java | 40 --- .../GizmoMemberAccessorImplementorTest.java | 23 +- .../domain/lookup/AbstractLookupTest.java | 2 +- .../cloner/gizmo/GizmoSolutionClonerTest.java | 4 +- .../GizmoMemberAccessorEntityEnhancer.java | 7 +- .../quarkus/deployment/TimefoldProcessor.java | 303 ++++++++---------- .../config/SolverBuildTimeConfig.java | 9 - ...TimefoldProcessorNearbySolverYamlTest.java | 2 - ...TimefoldProcessorSolverPropertiesTest.java | 2 - .../TimefoldProcessorSolverResourcesTest.java | 2 - .../TimefoldProcessorSolverYamlTest.java | 2 - .../TimefoldProcessorXMLDefaultTest.java | 2 - ...imefoldProcessorXMLNearbyPropertyTest.java | 2 - .../quarkus/TimefoldProcessorXMLNoneTest.java | 2 - .../TimefoldProcessorXMLPropertyTest.java | 2 - .../single-solver/application-nearby.yaml | 1 - .../quarkus/single-solver/application.yaml | 1 - .../TimefoldDevUIMultipleSolversTest.java | 1 - .../quarkus/it/devui/TimefoldDevUITest.java | 1 - .../TimefoldSolverAotContribution.java | 8 +- .../TimefoldSolverAutoConfiguration.java | 3 - .../config/SolverProperties.java | 18 -- .../autoconfigure/config/SolverProperty.java | 3 - ...efoldSolverGizmoAutoConfigurationTest.java | 75 ----- ...hSolverConfigXmlAutoConfigurationTest.java | 2 - .../single-solver/application.yaml | 1 - .../src/test/resources/solver-full.xml | 1 - .../src/main/resources/benchmark.xsd | 21 -- 53 files changed, 468 insertions(+), 571 deletions(-) rename core/src/main/java/ai/timefold/solver/core/{api => impl}/domain/common/DomainAccessType.java (58%) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoSupportStatus.java delete mode 100644 spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverGizmoAutoConfigurationTest.java diff --git a/core/pom.xml b/core/pom.xml index 91a2cf41acc..2241706a131 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -32,7 +32,6 @@ io.quarkus.gizmo gizmo2 - diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java b/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java index 9de9c49e3ce..01cf4b63f4b 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java @@ -26,7 +26,6 @@ import jakarta.xml.bind.annotation.XmlTransient; import jakarta.xml.bind.annotation.XmlType; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; @@ -73,7 +72,6 @@ "monitoringConfig", "solutionClass", "entityClassList", - "domainAccessType", "scoreDirectorFactoryConfig", "terminationConfig", "nearbyDistanceMeterClass", @@ -228,7 +226,6 @@ public class SolverConfig extends AbstractConfig { @XmlElement(name = "entityClass") protected List> entityClassList = null; - protected DomainAccessType domainAccessType = null; @XmlTransient protected Map gizmoMemberAccessorMap = null; @XmlTransient @@ -399,14 +396,6 @@ public void setEntityClassList(@Nullable List> entityClassList) { this.entityClassList = entityClassList; } - public @Nullable DomainAccessType getDomainAccessType() { - return domainAccessType; - } - - public void setDomainAccessType(@Nullable DomainAccessType domainAccessType) { - this.domainAccessType = domainAccessType; - } - public @Nullable Map<@NonNull String, @NonNull MemberAccessor> getGizmoMemberAccessorMap() { return gizmoMemberAccessorMap; } @@ -527,11 +516,6 @@ public void setMonitoringConfig(@Nullable MonitoringConfig monitoringConfig) { return this; } - public @NonNull SolverConfig withDomainAccessType(@NonNull DomainAccessType domainAccessType) { - this.domainAccessType = domainAccessType; - return this; - } - public @NonNull SolverConfig withGizmoMemberAccessorMap(@NonNull Map<@NonNull String, @NonNull MemberAccessor> memberAccessorMap) { this.gizmoMemberAccessorMap = memberAccessorMap; @@ -651,10 +635,6 @@ public boolean canTerminate() { return Objects.requireNonNullElse(environmentMode, EnvironmentMode.PHASE_ASSERT); } - public @NonNull DomainAccessType determineDomainAccessType() { - return Objects.requireNonNullElse(domainAccessType, DomainAccessType.REFLECTION); - } - public @NonNull MonitoringConfig determineMetricConfig() { return Objects.requireNonNullElse(monitoringConfig, new MonitoringConfig().withSolverMetricList(Arrays.asList(SolverMetric.SOLVE_DURATION, SolverMetric.ERROR_COUNT, @@ -698,7 +678,6 @@ public void offerRandomSeedFromSubSingleIndex(long subSingleIndex) { solutionClass = ConfigUtils.inheritOverwritableProperty(solutionClass, inheritedConfig.getSolutionClass()); entityClassList = ConfigUtils.inheritMergeableListProperty(entityClassList, inheritedConfig.getEntityClassList()); - domainAccessType = ConfigUtils.inheritOverwritableProperty(domainAccessType, inheritedConfig.getDomainAccessType()); gizmoMemberAccessorMap = ConfigUtils.inheritMergeableMapProperty( gizmoMemberAccessorMap, inheritedConfig.getGizmoMemberAccessorMap()); gizmoSolutionClonerMap = ConfigUtils.inheritMergeableMapProperty( diff --git a/core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java b/core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java index 3575d050d4d..7c71f8634da 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java +++ b/core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java @@ -30,10 +30,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.domain.common.PlanningId; import ai.timefold.solver.core.config.AbstractConfig; import ai.timefold.solver.core.impl.domain.common.AlphabeticMemberComparator; +import ai.timefold.solver.core.impl.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/common/DomainAccessType.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/DomainAccessType.java similarity index 58% rename from core/src/main/java/ai/timefold/solver/core/api/domain/common/DomainAccessType.java rename to core/src/main/java/ai/timefold/solver/core/impl/domain/common/DomainAccessType.java index 6de72354f6a..a0255cddaf6 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/domain/common/DomainAccessType.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/DomainAccessType.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.api.domain.common; +package ai.timefold.solver.core.impl.domain.common; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; @@ -7,25 +7,26 @@ * are accessed. */ public enum DomainAccessType { + /** + * Determine what domain access type to use automatically. + *

+ * This is the default. + */ + AUTO, + /** * Use reflection to read/write members (fields and methods) of the domain. *

* When used in a modulepath, the module must be open. * When used in GraalVM, the domain must be open for reflection. - *

- * This is the default, except with timefold-solver-quarkus. */ REFLECTION, + /** * Use Gizmo generated bytecode to access members (fields and methods) to avoid reflection * for additional performance. *

- * With timefold-solver-quarkus, this bytecode is generated at build time - * and it supports planning annotations on non-public members too. - *

- * Without timefold-solver-quarkus, this bytecode is generated at bootstrap runtime - * and you must add Gizmo in your classpath or modulepath - * and use planning annotations on public members only. + * This is the default when the application is run inside a JVM and not a native image. */ GIZMO } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactory.java index f5869249eb1..7462886dab8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactory.java @@ -10,15 +10,22 @@ import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.impl.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.AccessorInfo; import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoClassLoader; import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorFactory; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@NullMarked public final class MemberAccessorFactory { + static final Logger LOGGER = LoggerFactory.getLogger(MemberAccessorFactory.class); // exists only so that the various member accessors can share the same text in their exception messages static final String CLASSLOADER_NUDGE_MESSAGE = "Maybe add getClass().getClassLoader() as a parameter to the %s.create...() method call." @@ -33,7 +40,7 @@ public final class MemberAccessorFactory { * @param classLoader null or {@link GizmoClassLoader} if domainAccessType is {@link DomainAccessType#GIZMO}. * @return never null, new instance of the member accessor */ - public static MemberAccessor buildMemberAccessor(Member member, MemberAccessorType memberAccessorType, + private static MemberAccessor buildMemberAccessor(Member member, MemberAccessorType memberAccessorType, DomainAccessType domainAccessType, ClassLoader classLoader) { return buildMemberAccessor(member, memberAccessorType, null, domainAccessType, classLoader); } @@ -48,25 +55,27 @@ public static MemberAccessor buildMemberAccessor(Member member, MemberAccessorTy * @param classLoader null or {@link GizmoClassLoader} if domainAccessType is {@link DomainAccessType#GIZMO}. * @return never null, new instance of the member accessor */ - public static MemberAccessor buildMemberAccessor(Member member, MemberAccessorType memberAccessorType, - Class annotationClass, DomainAccessType domainAccessType, ClassLoader classLoader) { + static MemberAccessor buildMemberAccessor(Member member, MemberAccessorType memberAccessorType, + @Nullable Class annotationClass, DomainAccessType domainAccessType, ClassLoader classLoader) { return switch (domainAccessType) { + case AUTO -> throw new IllegalStateException( + "Impossible state: called with %s (AUTO) instead of a resolved domain access type" + .formatted(DomainAccessType.class.getSimpleName())); case GIZMO -> GizmoMemberAccessorFactory.buildGizmoMemberAccessor(member, annotationClass, - AccessorInfo.of(memberAccessorType != MemberAccessorType.VOID_METHOD, - memberAccessorType == MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER), + AccessorInfo.of(memberAccessorType), (GizmoClassLoader) Objects.requireNonNull(classLoader)); case REFLECTION -> buildReflectiveMemberAccessor(member, memberAccessorType, annotationClass); }; } private static MemberAccessor buildReflectiveMemberAccessor(Member member, MemberAccessorType memberAccessorType, - Class annotationClass) { + @Nullable Class annotationClass) { return buildReflectiveMemberAccessor(member, memberAccessorType, annotationClass, (AnnotatedElement) member); } private static MemberAccessor buildReflectiveMemberAccessor(Member member, MemberAccessorType memberAccessorType, - Class annotationClass, AnnotatedElement annotatedElement) { + @Nullable Class annotationClass, AnnotatedElement annotatedElement) { var messagePrefix = (annotationClass == null) ? "The" : "The @%s annotated".formatted(annotationClass.getSimpleName()); if (member instanceof Field field) { var getter = ReflectionHelper.getGetterMethod(field.getDeclaringClass(), field.getName()); @@ -154,6 +163,7 @@ private static MemberAccessor buildReflectiveMemberAccessor(Member member, Membe private final Map memberAccessorCache; private final GizmoClassLoader gizmoClassLoader = new GizmoClassLoader(); + private final boolean isGizmoSupported; public MemberAccessorFactory() { this(null); @@ -164,10 +174,16 @@ public MemberAccessorFactory() { * * @param memberAccessorMap key is the fully qualified member name */ - public MemberAccessorFactory(Map memberAccessorMap) { + public MemberAccessorFactory(@Nullable Map memberAccessorMap) { // The MemberAccessorFactory may be accessed, and this cache both read and updated, by multiple threads. this.memberAccessorCache = memberAccessorMap == null ? new ConcurrentHashMap<>() : new ConcurrentHashMap<>(memberAccessorMap); + // If the memberAccessorMap is not empty, we are in Quarkus using pregenerated member accessors + this.isGizmoSupported = + (memberAccessorMap != null && !memberAccessorMap.isEmpty()) || GizmoMemberAccessorFactory.isGizmoSupported( + gizmoClassLoader); + LOGGER.trace("Using domain access type {} for member accessors", + isGizmoSupported ? DomainAccessType.GIZMO : DomainAccessType.REFLECTION); } /** @@ -180,10 +196,16 @@ public MemberAccessorFactory(Map memberAccessorMap) { * @return never null, new {@link MemberAccessor} instance unless already found in memberAccessorMap */ public MemberAccessor buildAndCacheMemberAccessor(Member member, MemberAccessorType memberAccessorType, - Class annotationClass, DomainAccessType domainAccessType) { + @Nullable Class annotationClass, DomainAccessType domainAccessType) { String generatedClassName = GizmoMemberAccessorFactory.getGeneratedClassName(member); + if (domainAccessType == DomainAccessType.AUTO) { + domainAccessType = isGizmoSupported ? DomainAccessType.GIZMO : DomainAccessType.REFLECTION; + } + + var finalDomainAccessType = domainAccessType; return memberAccessorCache.computeIfAbsent(generatedClassName, - k -> MemberAccessorFactory.buildMemberAccessor(member, memberAccessorType, annotationClass, domainAccessType, + k -> MemberAccessorFactory.buildMemberAccessor(member, memberAccessorType, annotationClass, + finalDomainAccessType, gizmoClassLoader)); } @@ -198,8 +220,14 @@ public MemberAccessor buildAndCacheMemberAccessor(Member member, MemberAccessorT public MemberAccessor buildAndCacheMemberAccessor(Member member, MemberAccessorType memberAccessorType, DomainAccessType domainAccessType) { String generatedClassName = GizmoMemberAccessorFactory.getGeneratedClassName(member); + if (domainAccessType == DomainAccessType.AUTO) { + domainAccessType = isGizmoSupported ? DomainAccessType.GIZMO : DomainAccessType.REFLECTION; + } + + var finalDomainAccessType = domainAccessType; return memberAccessorCache.computeIfAbsent(generatedClassName, - k -> MemberAccessorFactory.buildMemberAccessor(member, memberAccessorType, domainAccessType, gizmoClassLoader)); + k -> MemberAccessorFactory.buildMemberAccessor(member, memberAccessorType, finalDomainAccessType, + gizmoClassLoader)); } public GizmoClassLoader getGizmoClassLoader() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java index 99a9ffbd7fc..0d02de96a62 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java @@ -34,7 +34,7 @@ public ReflectionBeanPropertyMemberAccessor(Method getterMethod, AnnotatedElemen this.annotatedElement = annotatedElement; MethodHandles.Lookup lookup = MethodHandles.lookup(); try { - getterMethod.setAccessible(true); + this.getterMethod.setAccessible(true); this.getherMethodHandle = lookup.unreflect(getterMethod) .asFixedArity(); } catch (IllegalAccessException e) { @@ -73,7 +73,7 @@ public ReflectionBeanPropertyMemberAccessor(Method getterMethod, AnnotatedElemen declaringClass.getCanonicalName())); } try { - setterMethod.setAccessible(true); + this.setterMethod.setAccessible(true); this.setterMethodHandle = lookup.unreflect(setterMethod) .asFixedArity(); } catch (IllegalAccessException e) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/AccessorInfo.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/AccessorInfo.java index ac86cdadf07..575747ef0bd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/AccessorInfo.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/AccessorInfo.java @@ -1,22 +1,27 @@ package ai.timefold.solver.core.impl.domain.common.accessor.gizmo; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; + /** * Additional information for the GIZMO accessor generation. * * @param returnTypeRequired a flag that indicates if the return type is required or optional * @param readMethodWithParameter a flag that allows the read method to accept an argument */ -public record AccessorInfo(boolean returnTypeRequired, boolean readMethodWithParameter) { +public record AccessorInfo(MemberAccessorFactory.MemberAccessorType memberAccessorType, boolean returnTypeRequired, + boolean readMethodWithParameter) { public static AccessorInfo withReturnValueAndNoArguments() { - return new AccessorInfo(true, false); + return new AccessorInfo(MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD, true, false); } public static AccessorInfo withReturnValueAndArguments() { - return new AccessorInfo(true, true); + return new AccessorInfo(MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, true, + true); } - public static AccessorInfo of(boolean returnTypeRequired, boolean readMethodWithParameter) { - return new AccessorInfo(returnTypeRequired, readMethodWithParameter); + public static AccessorInfo of(MemberAccessorFactory.MemberAccessorType memberAccessorType) { + return new AccessorInfo(memberAccessorType, memberAccessorType != MemberAccessorFactory.MemberAccessorType.VOID_METHOD, + memberAccessorType == MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java index 6ab553277ea..a648c677423 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java @@ -11,6 +11,7 @@ public final class GizmoClassLoader extends ClassLoader { private final Map classNameToBytecodeMap; + private GizmoSupportStatus gizmoSupportStatus; public GizmoClassLoader() { this(new HashMap<>()); @@ -24,6 +25,15 @@ public GizmoClassLoader(Map classNameToBytecodeMap) { */ super(GizmoClassLoader.class.getClassLoader()); this.classNameToBytecodeMap = classNameToBytecodeMap; + this.gizmoSupportStatus = GizmoSupportStatus.UNKNOWN; + } + + public GizmoSupportStatus getGizmoSupportStatus() { + return gizmoSupportStatus; + } + + public void setGizmoSupportStatus(GizmoSupportStatus gizmoSupportStatus) { + this.gizmoSupportStatus = gizmoSupportStatus; } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoFieldHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoFieldHandler.java index 979a40805b2..da23970f7d5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoFieldHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoFieldHandler.java @@ -36,18 +36,20 @@ final class GizmoFieldHandler implements GizmoMemberHandler { .formatted(fieldDescriptor.name(), declaringClass.getName())); } if (!ignoreChecks && !isFieldPublic) { - throw new IllegalStateException(""" - Member (%s) of class (%s) is not public.""" - .formatted(fieldDescriptor.name(), declaringClass.getName())); + throw new IllegalArgumentException( + """ + Member (%s) of class (%s) is not public.""" + .formatted(fieldDescriptor.name(), declaringClass.getName())); } getterDescriptor = null; setterDescriptor = null; this.canBeWritten = canBeWritten; } else { if (!ignoreChecks && !Modifier.isPublic(getterMethod.getModifiers())) { - throw new IllegalStateException(""" - Member (%s) of class (%s) is not public.""" - .formatted(getterMethod.getName(), getterMethod.getDeclaringClass().getName())); + throw new IllegalArgumentException( + """ + Member (%s) of class (%s) is not public.""" + .formatted(getterMethod.getName(), getterMethod.getDeclaringClass().getName())); } ReflectionHelper.assertGetterMethod(getterMethod); getterDescriptor = MethodDesc.of(getterMethod); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorFactory.java index 0a6c0c7ecec..57ef0b793d1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorFactory.java @@ -1,14 +1,20 @@ package ai.timefold.solver.core.impl.domain.common.accessor.gizmo; import java.lang.annotation.Annotation; +import java.lang.constant.ClassDesc; +import java.lang.invoke.MethodHandles; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; +import io.quarkus.gizmo2.Gizmo; +import io.quarkus.gizmo2.desc.ConstructorDesc; + public class GizmoMemberAccessorFactory { /** * Returns the generated class name for a given member. @@ -35,18 +41,56 @@ public static String getGeneratedClassName(Member member) { */ public static MemberAccessor buildGizmoMemberAccessor(Member member, Class annotationClass, AccessorInfo accessorInfo, GizmoClassLoader gizmoClassLoader) { - try { - // Check if Gizmo on the classpath by verifying we can access one of its classes - Class.forName("io.quarkus.gizmo2.Gizmo", false, - Thread.currentThread().getContextClassLoader()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException(""" - When using the domainAccessType (%s) the classpath or modulepath must contain io.quarkus.gizmo:gizmo2. - Maybe add a dependency to io.quarkus.gizmo:gizmo2.""".formatted(DomainAccessType.GIZMO)); - } return GizmoMemberAccessorImplementor.createAccessorFor(member, annotationClass, accessorInfo, gizmoClassLoader); } + public static boolean isGizmoSupported(GizmoClassLoader gizmoClassLoader) { + return switch (gizmoClassLoader.getGizmoSupportStatus()) { + case SUPPORTED -> true; + case UNSUPPORTED -> false; + case UNKNOWN -> { + var classPackage = GizmoMemberAccessorFactory.class.getPackage().getName(); + var bytecodeHolder = new AtomicReference(); + var gizmo = Gizmo.create((className, bytecode) -> { + bytecodeHolder.set(bytecode); + }); + + var classDesc = ClassDesc.of("%s.Test".formatted(classPackage)); + gizmo.class_(classDesc, classCreator -> { + classCreator.constructor(ConstructorDesc.of(classDesc), constructorCreator -> { + constructorCreator.public_(); + var this_ = constructorCreator.this_(); + constructorCreator.body(constructor -> { + constructor.invokeSpecial(ConstructorDesc.of(Object.class), this_); + constructor.return_(); + }); + }); + }); + try { + var generatedClass = MethodHandles.lookup().defineHiddenClass(bytecodeHolder.get(), true).lookupClass(); + var instance = generatedClass.getConstructor().newInstance(); + if (instance == null) { + // Should be impossible, but a native image might decide to optimize out + // instance if it is unused + gizmoClassLoader.setGizmoSupportStatus(GizmoSupportStatus.UNSUPPORTED); + yield false; + } else { + gizmoClassLoader.setGizmoSupportStatus(GizmoSupportStatus.SUPPORTED); + yield true; + } + } catch (IllegalAccessException | NoSuchMethodException | InstantiationException + | InvocationTargetException | Error e) { + // Note: GraalVM will throw a com.oracle.svm.core.jdk.UnsupportedFeatureError + // on defineHiddenClass, so we also catch "Error" here so we don't need + // to add GraalVM as a library + gizmoClassLoader.setGizmoSupportStatus(GizmoSupportStatus.UNSUPPORTED); + yield false; + } + } + }; + + } + private GizmoMemberAccessorFactory() { } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementor.java index d1526ce384c..ddb3125df1a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementor.java @@ -13,6 +13,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; import ai.timefold.solver.core.impl.util.MutableReference; import org.jspecify.annotations.NonNull; @@ -135,7 +136,7 @@ static MemberAccessor createAccessorFor(Member member, Class classBytecodeHolder = new MutableReference<>(null); ClassOutput classOutput = (path, byteCode) -> classBytecodeHolder.setValue(byteCode); - var descriptor = new GizmoMemberDescriptor(member, accessorInfo.readMethodWithParameter()); + var descriptor = new GizmoMemberDescriptor(member, accessorInfo); GizmoMemberInfo memberInfo = new GizmoMemberInfo(descriptor, accessorInfo.returnTypeRequired(), descriptor.getMethodParameterType() != null, annotationClass); defineAccessorFor(className, classOutput, memberInfo); @@ -235,35 +236,44 @@ private static void createGetDeclaringClass(GeneratedClassInfo generatedClassInf * 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 readMethodWithParameter Flag used to enable the method to accept an argument + * @param accessorInfo What kind of {@link MemberAccessor} is being generated */ - private static void assertIsGoodMethod(MethodDesc method, boolean returnTypeRequired, boolean readMethodWithParameter) { + private static void assertIsGoodMethod(MethodDesc method, AccessorInfo accessorInfo) { // V = void return type // Z = primitive boolean return type String methodName = method.name(); - if (!readMethodWithParameter && method.parameterCount() != 0) { + if (!accessorInfo.readMethodWithParameter() && method.parameterCount() != 0) { // not read or getter method throw new IllegalStateException("The getterMethod (%s) must not have any parameters, but has parameters (%s)." .formatted(methodName, Arrays.toString(method.parameterTypes().toArray()))); } - if (methodName.startsWith("get")) { - if (method.returnType().equals(ConstantDescs.CD_void)) { - throw new IllegalStateException("The getterMethod (%s) must have a non-void return type." - .formatted(methodName)); - } - } else if (methodName.startsWith("is")) { - if (!method.returnType().equals(ConstantDescs.CD_boolean)) { - throw new IllegalStateException(""" - The getterMethod (%s) must have a primitive boolean return type but returns (%s). - Maybe rename the method (get%s)?""" - .formatted(methodName, method.returnType(), methodName.substring(2))); + + var memberAccessorType = accessorInfo.memberAccessorType(); + + var methodType = (methodName.startsWith("get") || methodName.startsWith("is")) ? "getterMethod" : "readMethod"; + if (memberAccessorType == MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD || + memberAccessorType == MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER) { + // Only enforce getters/setters naming rules for getters/setters + // we don't want to enforce them on @ShadowSources suppliers + if (methodName.startsWith("get")) { + if (method.returnType().equals(ConstantDescs.CD_void)) { + throw new IllegalStateException("The getterMethod (%s) must have a non-void return type." + .formatted(methodName)); + } + } else if (methodName.startsWith("is")) { + if (!method.returnType().equals(ConstantDescs.CD_boolean)) { + throw new IllegalStateException(""" + The getterMethod (%s) must have a primitive boolean return type but returns (%s). + Maybe rename the method (get%s)?""" + .formatted(methodName, method.returnType(), methodName.substring(2))); + } } - } else { - // must be a read method - if (returnTypeRequired && method.returnType().equals(ConstantDescs.CD_void)) { - throw new IllegalStateException("The readMethod (%s) must have a non-void return type." - .formatted(methodName)); + } + if (memberAccessorType != MemberAccessorFactory.MemberAccessorType.VOID_METHOD) { + // must have a return type + if (method.returnType().equals(ConstantDescs.CD_void)) { + throw new IllegalStateException("The %s (%s) must have a non-void return type." + .formatted(methodType, methodName)); } } } @@ -271,41 +281,50 @@ 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 readMethodWithParameter Flag used to enable the method to accept an argument - * @param annotationClass Used in exception message + * @param method Method to assert is getter or read method + * @param accessorInfo What kind of {@link MemberAccessor} is being generated */ - private static void assertIsGoodMethod(MethodDesc method, boolean returnTypeRequired, boolean readMethodWithParameter, + private static void assertIsGoodMethod(MethodDesc method, AccessorInfo accessorInfo, Class annotationClass) { // V = void return type // Z = primitive boolean return type String methodName = method.name(); - if (!readMethodWithParameter && method.parameterCount() != 0) { + if (!accessorInfo.readMethodWithParameter() && method.parameterCount() != 0) { // not read or getter method throw new IllegalStateException( "The getterMethod (%s) with a %s annotation must not have any parameters, but has parameters (%s)." .formatted(methodName, annotationClass.getSimpleName(), method.parameterTypes().stream().map(ClassDesc::descriptorString).toList())); } - if (methodName.startsWith("get")) { - if (method.returnType().equals(ConstantDescs.CD_void)) { - throw new IllegalStateException("The getterMethod (%s) with a %s annotation must have a non-void return type." - .formatted(methodName, annotationClass.getSimpleName())); - } - } else if (methodName.startsWith("is")) { - if (!method.returnType().equals(ConstantDescs.CD_boolean)) { - throw new IllegalStateException(""" - The getterMethod (%s) with a %s annotation must have a primitive boolean return type but returns (%s). - Maybe rename the method (get%s)?""" - .formatted(methodName, annotationClass.getSimpleName(), method.returnType().descriptorString(), - methodName.substring(2))); + + var memberAccessorType = accessorInfo.memberAccessorType(); + var methodType = (methodName.startsWith("get") || methodName.startsWith("is")) ? "getterMethod" : "readMethod"; + + if (memberAccessorType == MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD || + memberAccessorType == MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER) { + if (methodName.startsWith("get")) { + if (method.returnType().equals(ConstantDescs.CD_void)) { + throw new IllegalStateException( + "The getterMethod (%s) with a @%s annotation must have a non-void return type." + .formatted(methodName, annotationClass.getSimpleName())); + } + } else if (methodName.startsWith("is")) { + if (!method.returnType().equals(ConstantDescs.CD_boolean)) { + throw new IllegalStateException( + """ + The %s (%s) with a @%s annotation must have a primitive boolean return type but returns (%s). + Maybe rename the method (get%s)?""" + .formatted(methodType, methodName, annotationClass.getSimpleName(), method.returnType() + .descriptorString(), + methodName.substring(2))); + } } - } else { - // must be a read method and return a result only if returnTypeRequired is true - if (returnTypeRequired && method.returnType().equals(ConstantDescs.CD_void)) { - throw new IllegalStateException("The readMethod (%s) with a %s annotation must have a non-void return type." - .formatted(methodName, annotationClass.getSimpleName())); + } + if (memberAccessorType != MemberAccessorFactory.MemberAccessorType.VOID_METHOD) { + // must be a read method + if (accessorInfo.returnTypeRequired() && method.returnType().equals(ConstantDescs.CD_void)) { + throw new IllegalStateException("The %s (%s) with a @%s annotation must have a non-void return type." + .formatted(methodType, methodName, annotationClass.getSimpleName())); } } } @@ -335,9 +354,9 @@ private static void createGetName(GeneratedClassInfo generatedClassInfo) { memberInfo.descriptor().whenIsMethod(method -> { var annotationClass = memberInfo.annotationClass(); if (annotationClass == null) { - assertIsGoodMethod(method, memberInfo.returnTypeRequired(), memberInfo.readMethodWithParameter()); + assertIsGoodMethod(method, memberInfo.descriptor().getAccessorInfo()); } else { - assertIsGoodMethod(method, memberInfo.returnTypeRequired(), memberInfo.readMethodWithParameter(), + assertIsGoodMethod(method, memberInfo.descriptor().getAccessorInfo(), annotationClass); } }); @@ -507,7 +526,7 @@ private static void createExecuteGetterWithParameter(GeneratedClassInfo generate var bean = builder.parameter("bean", Object.class); var value = builder.parameter("value", Object.class); memberInfo.descriptor().whenIsMethod( - md -> assertIsGoodMethod(md, memberInfo.returnTypeRequired(), memberInfo.readMethodWithParameter())); + md -> assertIsGoodMethod(md, memberInfo.descriptor().getAccessorInfo())); builder.body(blockCreator -> { var castedBean = blockCreator.localVar("castedBean", ClassDesc.of(memberInfo.descriptor().getDeclaringClassName()), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberDescriptor.java index f113abfb40e..05d628d9047 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberDescriptor.java @@ -10,6 +10,7 @@ import java.util.function.Consumer; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -40,6 +41,8 @@ public final class GizmoMemberDescriptor { @Nullable private final Type methodParameterType; + private final AccessorInfo accessorInfo; + private final GizmoMemberHandler memberHandler; /** @@ -54,11 +57,12 @@ public final class GizmoMemberDescriptor { private final MethodDesc setter; public GizmoMemberDescriptor(Member member) { - this(member, false); + this(member, AccessorInfo.withReturnValueAndArguments()); } - public GizmoMemberDescriptor(Member member, boolean methodWithParameter) { + public GizmoMemberDescriptor(Member member, AccessorInfo accessorInfo) { Class declaringClass = member.getDeclaringClass(); + this.accessorInfo = accessorInfo; if (member instanceof Field field) { var fieldDescriptor = FieldDesc.of(field); this.name = member.getName(); @@ -74,7 +78,7 @@ public GizmoMemberDescriptor(Member member, boolean methodWithParameter) { var methodDescriptor = MethodDesc.of(method); this.name = ReflectionHelper.isGetterMethod(method) ? ReflectionHelper.getGetterPropertyName(member) : member.getName(); - this.methodParameterType = getMethodParameterType(method, methodWithParameter); + this.methodParameterType = getMethodParameterType(method, accessorInfo.readMethodWithParameter()); this.memberHandler = GizmoMemberHandler.of(declaringClass, (Class) methodParameterType, methodDescriptor); this.setter = lookupSetter(methodDescriptor, declaringClass, name).orElse(null); } else { @@ -83,31 +87,36 @@ public GizmoMemberDescriptor(Member member, boolean methodWithParameter) { this.metadataHandler = this.memberHandler; } - public GizmoMemberDescriptor(String name, FieldDesc fieldDescriptor, Class declaringClass) { + public GizmoMemberDescriptor(String name, FieldDesc fieldDescriptor, Class declaringClass, + AccessorInfo accessorInfo) { this.name = name; this.memberHandler = GizmoMemberHandler.of(declaringClass, name, fieldDescriptor, true); this.metadataHandler = this.memberHandler; this.setter = null; this.methodParameterType = null; + this.accessorInfo = accessorInfo; } public GizmoMemberDescriptor(String name, MethodDesc memberDescriptor, MethodDesc metadataDescriptor, - Type methodParameterType, Class declaringClass, @Nullable MethodDesc setterDescriptor) { + Type methodParameterType, Class declaringClass, @Nullable MethodDesc setterDescriptor, + AccessorInfo accessorInfo) { this.name = name; this.memberHandler = GizmoMemberHandler.of(declaringClass, (Class) methodParameterType, memberDescriptor); this.metadataHandler = memberDescriptor == metadataDescriptor ? this.memberHandler : GizmoMemberHandler.of(declaringClass, metadataDescriptor); this.methodParameterType = methodParameterType; this.setter = setterDescriptor; + this.accessorInfo = accessorInfo; } public GizmoMemberDescriptor(String name, MethodDesc memberDescriptor, Type methodParameterType, Class declaringClass, - @Nullable MethodDesc setterDescriptor) { + @Nullable MethodDesc setterDescriptor, AccessorInfo accessorInfo) { this.name = name; this.memberHandler = GizmoMemberHandler.of(declaringClass, (Class) methodParameterType, memberDescriptor); this.metadataHandler = this.memberHandler; this.methodParameterType = methodParameterType; this.setter = setterDescriptor; + this.accessorInfo = accessorInfo; } public GizmoMemberDescriptor(String name, MethodDesc memberDescriptor, FieldDesc metadataDescriptor, @@ -117,6 +126,7 @@ public GizmoMemberDescriptor(String name, MethodDesc memberDescriptor, FieldDesc this.metadataHandler = GizmoMemberHandler.of(declaringClass, name, metadataDescriptor, true); this.methodParameterType = methodParameterType; this.setter = setterDescriptor; + this.accessorInfo = AccessorInfo.of(MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD); } @Nullable @@ -251,6 +261,10 @@ public Type getMethodParameterType() { return methodParameterType; } + public AccessorInfo getAccessorInfo() { + return accessorInfo; + } + @Override public String toString() { return memberHandler.toString(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoSupportStatus.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoSupportStatus.java new file mode 100644 index 00000000000..f27f8a830fd --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoSupportStatus.java @@ -0,0 +1,7 @@ +package ai.timefold.solver.core.impl.domain.common.accessor.gizmo; + +public enum GizmoSupportStatus { + SUPPORTED, + UNSUPPORTED, + UNKNOWN; +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpStrategyResolver.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpStrategyResolver.java index 8f70cfb4486..0060cadb565 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpStrategyResolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpStrategyResolver.java @@ -3,9 +3,9 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.domain.common.PlanningId; import ai.timefold.solver.core.config.util.ConfigUtils; +import ai.timefold.solver.core.impl.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; import ai.timefold.solver.core.impl.domain.solution.cloner.DeepCloningUtils; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/policy/DescriptorPolicy.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/policy/DescriptorPolicy.java index 5568a77558e..3e843b2eaac 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/policy/DescriptorPolicy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/policy/DescriptorPolicy.java @@ -12,7 +12,6 @@ import java.util.Map; import java.util.Set; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.domain.solution.PlanningScore; import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; @@ -27,6 +26,7 @@ import ai.timefold.solver.core.api.score.SimpleBigDecimalScore; import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.config.solver.PreviewFeature; +import ai.timefold.solver.core.impl.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightSupplier.java index 1de76faa1c3..175f977ca0d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightSupplier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightSupplier.java @@ -2,11 +2,11 @@ import java.util.Set; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.impl.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/OverridesBasedConstraintWeightSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/OverridesBasedConstraintWeightSupplier.java index 0c6c8f66dc3..65ca1be1437 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/OverridesBasedConstraintWeightSupplier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/OverridesBasedConstraintWeightSupplier.java @@ -6,11 +6,11 @@ import java.util.Set; import java.util.stream.Collectors; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.impl.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/cloner/gizmo/GizmoSolutionClonerFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/cloner/gizmo/GizmoSolutionClonerFactory.java index f52bb8c9382..ec6a1ca8718 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/cloner/gizmo/GizmoSolutionClonerFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/cloner/gizmo/GizmoSolutionClonerFactory.java @@ -1,6 +1,5 @@ package ai.timefold.solver.core.impl.domain.solution.cloner.gizmo; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner; import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoClassLoader; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; @@ -19,14 +18,6 @@ public static String getGeneratedClassName(SolutionDescriptor solutionDescrip } public static SolutionCloner build(SolutionDescriptor solutionDescriptor, GizmoClassLoader gizmoClassLoader) { - try { - // Check if Gizmo on the classpath by verifying we can access one of its classes - Class.forName("io.quarkus.gizmo2.Gizmo", false, Thread.currentThread().getContextClassLoader()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException(""" - When using the domainAccessType (%s) the classpath or modulepath must contain io.quarkus.gizmo:gizmo2. - Maybe add a dependency to io.quarkus.gizmo:gizmo2.""".formatted(DomainAccessType.GIZMO)); - } return new GizmoSolutionClonerImplementor().createClonerFor(solutionDescriptor, gizmoClassLoader); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java index 6e7e38af6dc..73355fdaedc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java @@ -35,7 +35,6 @@ import java.util.stream.Stream; import ai.timefold.solver.core.api.domain.autodiscover.AutoDiscoverMemberType; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides; import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; @@ -50,6 +49,7 @@ import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.config.util.ConfigUtils; +import ai.timefold.solver.core.impl.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; @@ -723,17 +723,11 @@ private void initSolutionCloner(DescriptorPolicy descriptorPolicy) { gizmoSolutionCloner.setSolutionDescriptor(this); } if (solutionCloner == null) { - switch (descriptorPolicy.getDomainAccessType()) { - case GIZMO: - solutionCloner = GizmoSolutionClonerFactory.build(this, memberAccessorFactory.getGizmoClassLoader()); - break; - case REFLECTION: - solutionCloner = new FieldAccessingSolutionCloner<>(this); - break; - default: - throw new IllegalStateException("The domainAccessType (" + domainAccessType - + ") is not implemented."); - } + solutionCloner = switch (descriptorPolicy.getDomainAccessType()) { + case GIZMO -> GizmoSolutionClonerFactory.build(this, memberAccessorFactory.getGizmoClassLoader()); + // AUTO means we are probably in plain Java, so we need to use reflection so we can clone final fields + case AUTO, REFLECTION -> new FieldAccessingSolutionCloner<>(this); + }; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java index 12b29585f57..c2952c8f30e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java @@ -26,6 +26,7 @@ import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.impl.AbstractFromConfigFactory; import ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhaseFactory; +import ai.timefold.solver.core.impl.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; @@ -189,7 +190,7 @@ private SolutionDescriptor buildSolutionDescriptor() { .formatted(solverConfig.getEntityClassList())); } return SolutionDescriptor.buildSolutionDescriptor(solverConfig.getEnablePreviewFeatureSet(), - solverConfig.determineDomainAccessType(), + DomainAccessType.AUTO, (Class) solverConfig.getSolutionClass(), solverConfig.getGizmoMemberAccessorMap(), solverConfig.getGizmoSolutionClonerMap(), diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 1e68b872d4b..3aff40417d8 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -35,8 +35,6 @@ - - @@ -1541,18 +1539,6 @@ - - - - - - - - - - - - diff --git a/core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java b/core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java index 549469c6f9c..f7912816be7 100644 --- a/core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java +++ b/core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java @@ -299,7 +299,7 @@ private record DummyRecordSolution( } @PlanningSolution - private static class DummySolutionWithRecordEntity { + public static class DummySolutionWithRecordEntity { @PlanningEntityCollectionProperty List entities; @@ -358,7 +358,7 @@ private static class DummyEntityWithMixedSimpleAndListVariable { } @PlanningSolution - private static class DummySolutionWithTwoListVariablesEntity { + public static class DummySolutionWithTwoListVariablesEntity { @PlanningEntityCollectionProperty List entities; @@ -409,7 +409,7 @@ public void setScore(SimpleScore score) { } @PlanningEntity - private static class DummyEntityWithTwoListVariables { + public static class DummyEntityWithTwoListVariables { @PlanningListVariable(valueRangeProviderRefs = "firstListValueRange") private List firstListVariable; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactoryTest.java index 505175edac8..3cfbc17386e 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactoryTest.java @@ -9,9 +9,9 @@ import java.util.HashMap; import java.util.Map; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.domain.solution.ProblemFactProperty; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.impl.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorFactory; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataValue; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java index 323dddfa2dc..f45cd023380 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java @@ -4,12 +4,14 @@ import static org.assertj.core.api.Assertions.assertThatCode; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorFactory; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataValue; import ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataDifferentGetterSetterVisibilityEntity; import ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataInvalidGetterEntity; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; class ReflectionBeanPropertyMemberAccessorTest { @@ -31,19 +33,27 @@ void methodAnnotatedEntity() throws NoSuchMethodException { @Test void getterSetterVisibilityDoesNotMatch() { - assertThatCode(() -> new ReflectionBeanPropertyMemberAccessor( - TestdataDifferentGetterSetterVisibilityEntity.class.getDeclaredMethod("getValue1"))) - .hasMessageContainingAll("getterMethod (getValue1)", - "has access modifier (public)", - "not match the setterMethod (setValue1)", - "access modifier (private)", - "on class (ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataDifferentGetterSetterVisibilityEntity)"); - assertThatCode(() -> new ReflectionBeanPropertyMemberAccessor( - TestdataDifferentGetterSetterVisibilityEntity.class.getDeclaredMethod("getValue2"))) - .hasMessageContainingAll("getterMethod (getValue2)", - "on class (%s)".formatted(TestdataDifferentGetterSetterVisibilityEntity.class.getCanonicalName()), - "is not public", - "having access modifier (package-private) instead."); + try (var gizmoMemberAccessorFactoryMock = Mockito.mockStatic(GizmoMemberAccessorFactory.class)) { + // Mock GizmoMemberAccessorFactory so MemberAccessorFactory think we are in a native image + gizmoMemberAccessorFactoryMock.when(() -> GizmoMemberAccessorFactory.isGizmoSupported(Mockito.any())) + .thenReturn(false); + gizmoMemberAccessorFactoryMock.when(() -> GizmoMemberAccessorFactory.getGeneratedClassName(Mockito.any())) + .thenCallRealMethod(); + + assertThatCode(() -> new ReflectionBeanPropertyMemberAccessor( + TestdataDifferentGetterSetterVisibilityEntity.class.getDeclaredMethod("getValue1"))) + .hasMessageContainingAll("getterMethod (getValue1)", + "has access modifier (public)", + "not match the setterMethod (setValue1)", + "access modifier (private)", + "on class (ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataDifferentGetterSetterVisibilityEntity)"); + assertThatCode(() -> new ReflectionBeanPropertyMemberAccessor( + TestdataDifferentGetterSetterVisibilityEntity.class.getDeclaredMethod("getValue2"))) + .hasMessageContainingAll("getterMethod (getValue2)", + "on class (%s)".formatted(TestdataDifferentGetterSetterVisibilityEntity.class.getCanonicalName()), + "is not public", + "having access modifier (package-private) instead."); + } } @Test diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodExtendedMemberAccessorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodExtendedMemberAccessorTest.java index f2933c33d3a..2c495945870 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodExtendedMemberAccessorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodExtendedMemberAccessorTest.java @@ -6,6 +6,7 @@ import java.util.List; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorFactory; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.parameter.TestdataEntityProvidingWithParameterEntity; @@ -19,6 +20,7 @@ import ai.timefold.solver.core.testdomain.valuerange.parameter.invalid.TestdataInvalidParameterSolution; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; class ReflectionMethodExtendedMemberAccessorTest { @@ -74,28 +76,45 @@ void assertMemberWithInheritance(ReflectionMethodExtendedMemberAccessor member, @Test void invalidEntityReadMethodWithParameter() { - assertThatCode(TestdataInvalidTypeEntityProvidingWithParameterEntity::buildVariableDescriptorForValueRange) - .hasMessageContaining("The parameter type (%s)".formatted(TestdataSolution.class.getCanonicalName())) - .hasMessageContaining( - "of the method (getValueRange) must match the solution (%s)." - .formatted(TestdataInvalidTypeEntityProvidingWithParameterSolution.class.getCanonicalName())); - assertThatCode(TestdataInvalidCountEntityProvidingWithParameterEntity::buildVariableDescriptorForValueRange) - .hasMessageContaining("The readMethod") - .hasMessageContaining("with a @%s annotation must have only one parameter" - .formatted(ValueRangeProvider.class.getSimpleName())); + try (var gizmoMemberAccessorFactoryMock = Mockito.mockStatic(GizmoMemberAccessorFactory.class)) { + // Mock GizmoMemberAccessorFactory so MemberAccessorFactory think we are in a native image + gizmoMemberAccessorFactoryMock.when(() -> GizmoMemberAccessorFactory.isGizmoSupported(Mockito.any())) + .thenReturn(false); + gizmoMemberAccessorFactoryMock.when(() -> GizmoMemberAccessorFactory.getGeneratedClassName(Mockito.any())) + .thenCallRealMethod(); + + assertThatCode(TestdataInvalidTypeEntityProvidingWithParameterEntity::buildVariableDescriptorForValueRange) + .hasMessageContaining("The parameter type (%s)".formatted(TestdataSolution.class.getCanonicalName())) + .hasMessageContaining( + "of the method (getValueRange) must match the solution (%s)." + .formatted( + TestdataInvalidTypeEntityProvidingWithParameterSolution.class.getCanonicalName())); + assertThatCode(TestdataInvalidCountEntityProvidingWithParameterEntity::buildVariableDescriptorForValueRange) + .hasMessageContaining("The readMethod") + .hasMessageContaining("with a @%s annotation must have only one parameter" + .formatted(ValueRangeProvider.class.getSimpleName())); + } } @Test void invalidSolutionReadMethodWithParameter() { - assertThatCode(TestdataInvalidParameterSolution::buildSolutionDescriptor) - .hasMessageContainingAll( - "The readMethod (public java.util.List %s.getValueList(%s))" - .formatted(TestdataInvalidParameterSolution.class.getCanonicalName(), - TestdataInvalidParameterSolution.class.getCanonicalName())) - .hasMessageContainingAll( - " with a @%s annotation must not have any parameters ([class %s])." - .formatted(ValueRangeProvider.class.getSimpleName(), - TestdataInvalidParameterSolution.class.getCanonicalName())); + try (var gizmoMemberAccessorFactoryMock = Mockito.mockStatic(GizmoMemberAccessorFactory.class)) { + // Mock GizmoMemberAccessorFactory so MemberAccessorFactory think we are in a native image + gizmoMemberAccessorFactoryMock.when(() -> GizmoMemberAccessorFactory.isGizmoSupported(Mockito.any())) + .thenReturn(false); + gizmoMemberAccessorFactoryMock.when(() -> GizmoMemberAccessorFactory.getGeneratedClassName(Mockito.any())) + .thenCallRealMethod(); + + assertThatCode(TestdataInvalidParameterSolution::buildSolutionDescriptor) + .hasMessageContainingAll( + "The readMethod (public java.util.List %s.getValueList(%s))" + .formatted(TestdataInvalidParameterSolution.class.getCanonicalName(), + TestdataInvalidParameterSolution.class.getCanonicalName())) + .hasMessageContainingAll( + " with a @%s annotation must not have any parameters ([class %s])." + .formatted(ValueRangeProvider.class.getSimpleName(), + TestdataInvalidParameterSolution.class.getCanonicalName())); + } } @Test diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorFactoryTest.java index 2d94b9f0a4c..17a63938bc9 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorFactoryTest.java @@ -1,9 +1,7 @@ package ai.timefold.solver.core.impl.domain.common.accessor.gizmo; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import java.lang.reflect.Member; import java.lang.reflect.Method; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; @@ -11,24 +9,9 @@ import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataValue; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class GizmoMemberAccessorFactoryTest { - - static ClassLoader contextClassLoader; - - @BeforeAll - static void setContextClassLoader() { - contextClassLoader = Thread.currentThread().getContextClassLoader(); - } - - @BeforeEach - void setup() { - Thread.currentThread().setContextClassLoader(contextClassLoader); - } - // Duplicates GizmoMemberAccessorImplementor test, // but this is making sure the member accessor returned // is the one from GizmoMemberAccessorImplementor @@ -55,27 +38,4 @@ void testReturnedMemberAccessor() throws NoSuchMethodException { assertThat(memberAccessor.getName()).isEqualTo("value"); assertThat(memberAccessor.getSpeedNote()).isEqualTo("Fast access with generated bytecode"); } - - @Test - void testGizmoNotOnClasspathThrowsException() throws NoSuchMethodException { - Member member = TestdataEntity.class.getMethod("getValue"); - Thread.currentThread().setContextClassLoader(new ClassLoader() { - @Override - public String getName() { - return "ClassLoader without Gizmo"; - } - - @Override - public Class loadClass(String name) { - return null; - } - }); - - assertThatCode(() -> { - GizmoMemberAccessorFactory.buildGizmoMemberAccessor(member, PlanningVariable.class, - AccessorInfo.withReturnValueAndNoArguments(), - new GizmoClassLoader()); - }).hasMessage("When using the domainAccessType (GIZMO) the classpath or modulepath must contain " + - "io.quarkus.gizmo:gizmo2.\nMaybe add a dependency to io.quarkus.gizmo:gizmo2."); - } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementorTest.java index c8119065099..d000559a9cd 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementorTest.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.api.domain.entity.PlanningPin; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataValue; import ai.timefold.solver.core.testdomain.gizmo.GizmoTestdataEntity; @@ -132,7 +133,8 @@ void testGeneratedMemberAccessorSameClass() throws NoSuchMethodException { void testGeneratedMemberAccessorReturnVoid() throws NoSuchMethodException { var member = TestdataEntity.class.getMethod("updateValue"); var memberAccessor = - GizmoMemberAccessorFactory.buildGizmoMemberAccessor(member, null, AccessorInfo.of(false, false), + GizmoMemberAccessorFactory.buildGizmoMemberAccessor(member, null, AccessorInfo.of( + MemberAccessorFactory.MemberAccessorType.VOID_METHOD), new GizmoClassLoader()); var entity = new TestdataEntity(); @@ -169,7 +171,9 @@ void testThrowsWhenGetterMethodReturnVoid() throws NoSuchMethodException { GizmoMemberAccessorImplementor.createAccessorFor(member, PlanningVariable.class, AccessorInfo.withReturnValueAndNoArguments(), new GizmoClassLoader()); - }).hasMessage("The getterMethod (getVoid) with a PlanningVariable annotation must have a non-void return type."); + }).hasMessageContainingAll("The getterMethod (getVoid)", + "with a @%s annotation".formatted(PlanningVariable.class.getSimpleName()), + "must have a non-void return type."); } @Test @@ -179,7 +183,9 @@ void testThrowsWhenReadMethodReturnVoid() throws NoSuchMethodException { GizmoMemberAccessorImplementor.createAccessorFor(member, PlanningVariable.class, AccessorInfo.withReturnValueAndNoArguments(), new GizmoClassLoader()); - }).hasMessage("The readMethod (voidMethod) with a PlanningVariable annotation must have a non-void return type."); + }).hasMessageContainingAll("The readMethod (voidMethod)", + "with a @%s annotation".formatted(PlanningVariable.class.getSimpleName()), + "must have a non-void return type."); } @Test @@ -208,13 +214,12 @@ void testThrowsWhenGetBooleanReturnsNonBoolean() throws NoSuchMethodException { var member = GizmoTestdataEntity.class.getMethod("isAMethodThatHasABadName"); assertThatCode( () -> GizmoMemberAccessorImplementor.createAccessorFor(member, PlanningVariable.class, - AccessorInfo.withReturnValueAndNoArguments(), + AccessorInfo.of(MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD), new GizmoClassLoader())) - .hasMessage(""" - The getterMethod (isAMethodThatHasABadName) with a PlanningVariable annotation \ - must have a primitive boolean return type but returns (L%s;). - Maybe rename the method (getAMethodThatHasABadName)?""" - .formatted(String.class.getName().replace('.', '/'))); + .hasMessageContainingAll("The getterMethod (isAMethodThatHasABadName)", + "with a @%s annotation".formatted(PlanningVariable.class.getSimpleName()), + "must have a primitive boolean return type but returns (L%s;)".formatted( + String.class.getName().replace('.', '/'))); } @Test diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/lookup/AbstractLookupTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/lookup/AbstractLookupTest.java index 3642a800975..95335565945 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/lookup/AbstractLookupTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/lookup/AbstractLookupTest.java @@ -1,6 +1,6 @@ package ai.timefold.solver.core.impl.domain.lookup; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; +import ai.timefold.solver.core.impl.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/cloner/gizmo/GizmoSolutionClonerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/cloner/gizmo/GizmoSolutionClonerTest.java index 3c0346d1185..4ed5a838af8 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/cloner/gizmo/GizmoSolutionClonerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/cloner/gizmo/GizmoSolutionClonerTest.java @@ -13,6 +13,7 @@ import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; +import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.AccessorInfo; import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoClassLoader; import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoMemberDescriptor; import ai.timefold.solver.core.impl.domain.solution.cloner.AbstractSolutionClonerTest; @@ -99,7 +100,8 @@ private GizmoSolutionOrEntityDescriptor generateGizmoSolutionOrEntityDescriptor( var name = field.getName(); if (Modifier.isPublic(field.getModifiers())) { - member = new GizmoMemberDescriptor(name, memberDescriptor, declaringClass); + member = new GizmoMemberDescriptor(name, memberDescriptor, declaringClass, + AccessorInfo.withReturnValueAndNoArguments()); } else { var getter = ReflectionHelper.getGetterMethod(currentClass, field.getName()); var setter = ReflectionHelper.getSetterMethod(currentClass, field.getName()); diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/GizmoMemberAccessorEntityEnhancer.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/GizmoMemberAccessorEntityEnhancer.java index 5be8a963778..32c210d14a2 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/GizmoMemberAccessorEntityEnhancer.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/GizmoMemberAccessorEntityEnhancer.java @@ -192,7 +192,7 @@ public String generateMethodAccessor(@Nullable AnnotationInstance annotationInst GizmoMemberDescriptor.getMethodParameterType(methodMember, accessorInfo.readMethodWithParameter()); if (Modifier.isPublic(methodInfo.flags())) { descriptor = new GizmoMemberDescriptor(name, memberDescriptor, methodParameterType, declaringClass, - setterDescriptor.orElse(null)); + setterDescriptor.orElse(null), accessorInfo); } else { setterDescriptor = addVirtualMethodGetter(classInfo, methodInfo, name, setterDescriptor.orElse(null), transformers); var methodName = getVirtualGetterName(false, name); @@ -201,7 +201,7 @@ public String generateMethodAccessor(@Nullable AnnotationInstance annotationInst memberDescriptor.returnType()); descriptor = new GizmoMemberDescriptor(name, newMethodDescriptor, memberDescriptor, methodParameterType, declaringClass, - setterDescriptor.orElse(null)); + setterDescriptor.orElse(null), accessorInfo); } Class annotationClass = null; if (accessorInfo.returnTypeRequired() || annotationInstance != null) { @@ -387,7 +387,8 @@ private GizmoMemberDescriptor createMemberDescriptorForField(Field field, // Not being recorded, so can use Type and annotated element directly if ((ReflectionHelper.hasGetterMethod(declaringClass, name) || Modifier.isPublic(field.getModifiers())) && !isForCloning) { - return new GizmoMemberDescriptor(name, memberDescriptor, declaringClass); + return new GizmoMemberDescriptor(name, memberDescriptor, declaringClass, + AccessorInfo.withReturnValueAndNoArguments()); } else { addVirtualFieldGetterAndSetter(declaringClass, field, transformers); var getterName = getVirtualGetterName(true, name); diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java index 773a32ef995..7ead7f84242 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java @@ -1,7 +1,6 @@ package ai.timefold.solver.quarkus.deployment; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; -import static java.lang.String.format; import java.lang.constant.ClassDesc; import java.lang.reflect.Field; @@ -26,7 +25,6 @@ import jakarta.inject.Singleton; import ai.timefold.solver.core.api.domain.autodiscover.AutoDiscoverMemberType; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; import ai.timefold.solver.core.api.domain.solution.PlanningScore; @@ -44,7 +42,9 @@ import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.config.solver.SolverManagerConfig; +import ai.timefold.solver.core.impl.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.AccessorInfo; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.variable.declarative.RootVariableSource; @@ -610,14 +610,9 @@ void buildConstraintMetaModel(SolverConfigBuildItem solverConfigBuildItem, var constraintMetaModelsBySolverNames = new HashMap(); solverConfigBuildItem.getSolverConfigMap().forEach((solverName, solverConfig) -> { - // Gizmo-generated member accessors are not yet available at build time. - var originalDomainAccessType = solverConfig.getDomainAccessType(); - solverConfig.setDomainAccessType(DomainAccessType.REFLECTION); - var solverFactory = SolverFactory.create(solverConfig); var constraintMetaModel = BeanUtil.buildConstraintMetaModel(solverFactory); // Avoid changing the original solver config. - solverConfig.setDomainAccessType(originalDomainAccessType); constraintMetaModelsBySolverNames.put(solverName, constraintMetaModel); }); @@ -804,13 +799,6 @@ private void applySolverProperties(IndexView indexView, String solverName, Solve applyScoreDirectorFactoryProperties(indexView, solverConfig); // Override the current configuration with values from the solver properties - timefoldBuildTimeConfig.getSolverConfig(solverName).flatMap(SolverBuildTimeConfig::domainAccessType) - .ifPresent(solverConfig::setDomainAccessType); - - if (solverConfig.getDomainAccessType() == null) { - solverConfig.setDomainAccessType(DomainAccessType.GIZMO); - } - timefoldBuildTimeConfig.getSolverConfig(solverName) .flatMap(SolverBuildTimeConfig::enabledPreviewFeatures) .ifPresent(solverConfig::setEnablePreviewFeatureSet); @@ -981,155 +969,157 @@ private GeneratedGizmoClasses generateDomainAccessors(Map * "entity" in this context means both "planning solution", * "planning entity" and other things as well. */ - assertSolverDomainAccessType(solverConfigMap); var entityEnhancer = new GizmoMemberAccessorEntityEnhancer(); - if (solverConfigMap.values().stream().anyMatch(c -> c.getDomainAccessType() == DomainAccessType.GIZMO)) { - var membersToGeneratedAccessorsForCollection = new ArrayList(); + var membersToGeneratedAccessorsForCollection = new ArrayList(); + + // Every entity and solution gets scanned for annotations. + // Annotated members get their accessors generated. + for (var dotName : DotNames.GIZMO_MEMBER_ACCESSOR_ANNOTATIONS) { + membersToGeneratedAccessorsForCollection.addAll(indexView.getAnnotationsWithRepeatable(dotName, indexView)); + } + generateDomainAccessorsForShadowSources(indexView, membersToGeneratedAccessorsForCollection); + membersToGeneratedAccessorsForCollection.removeIf(this::shouldIgnoreMember); + + // Fail fast on auto-discovery. + var planningSolutionAnnotationInstanceCollection = getAllConcreteSolutionClasses(indexView); + var unconfiguredSolverConfigList = solverConfigMap.entrySet().stream() + .filter(e -> e.getValue().getSolutionClass() == null) + .map(Map.Entry::getKey) + .toList(); + var unusedSolutionClassList = planningSolutionAnnotationInstanceCollection.stream() + .map(planningClass -> planningClass.target().asClass().name().toString()) + .filter(planningClassName -> reflectiveClassSet.stream() + .noneMatch(clazz -> clazz.getName().equals(planningClassName))) + .toList(); + if (planningSolutionAnnotationInstanceCollection.isEmpty()) { + throw new IllegalStateException( + "No classes found with a @%s annotation.".formatted(PlanningSolution.class.getSimpleName())); + } else if (planningSolutionAnnotationInstanceCollection.size() > 1 && !unconfiguredSolverConfigList.isEmpty() + && !unusedSolutionClassList.isEmpty()) { + throw new IllegalStateException( + "Unused classes (%s) found with a @%s annotation.".formatted(String.join(", ", unusedSolutionClassList), + PlanningSolution.class.getSimpleName())); + } - // Every entity and solution gets scanned for annotations. - // Annotated members get their accessors generated. - for (var dotName : DotNames.GIZMO_MEMBER_ACCESSOR_ANNOTATIONS) { - membersToGeneratedAccessorsForCollection.addAll(indexView.getAnnotationsWithRepeatable(dotName, indexView)); + planningSolutionAnnotationInstanceCollection.forEach(planningSolutionAnnotationInstance -> { + var autoDiscoverMemberType = planningSolutionAnnotationInstance.values().stream() + .filter(v -> v.name().equals("autoDiscoverMemberType")) + .findFirst() + .map(AnnotationValue::asEnum) + .map(AutoDiscoverMemberType::valueOf) + .orElse(AutoDiscoverMemberType.NONE); + + if (autoDiscoverMemberType != AutoDiscoverMemberType.NONE) { + throw new UnsupportedOperationException(""" + Auto-discovery of members using %s is not supported under Quarkus. + Remove the autoDiscoverMemberType property from the @%s annotation + and explicitly annotate the fields or getters with annotations such as @%s, @%s or @%s.""" + .strip() + .formatted( + AutoDiscoverMemberType.class.getSimpleName(), + PlanningSolution.class.getSimpleName(), + PlanningScore.class.getSimpleName(), + PlanningEntityCollectionProperty.class.getSimpleName(), + ProblemFactCollectionProperty.class.getSimpleName())); } - generateDomainAccessorsForShadowSources(indexView, membersToGeneratedAccessorsForCollection); - membersToGeneratedAccessorsForCollection.removeIf(this::shouldIgnoreMember); - - // Fail fast on auto-discovery. - var planningSolutionAnnotationInstanceCollection = getAllConcreteSolutionClasses(indexView); - var unconfiguredSolverConfigList = solverConfigMap.entrySet().stream() - .filter(e -> e.getValue().getSolutionClass() == null) - .map(Map.Entry::getKey) - .toList(); - var unusedSolutionClassList = planningSolutionAnnotationInstanceCollection.stream() - .map(planningClass -> planningClass.target().asClass().name().toString()) - .filter(planningClassName -> reflectiveClassSet.stream() - .noneMatch(clazz -> clazz.getName().equals(planningClassName))) - .toList(); - if (planningSolutionAnnotationInstanceCollection.isEmpty()) { - throw new IllegalStateException( - "No classes found with a @%s annotation.".formatted(PlanningSolution.class.getSimpleName())); - } else if (planningSolutionAnnotationInstanceCollection.size() > 1 && !unconfiguredSolverConfigList.isEmpty() - && !unusedSolutionClassList.isEmpty()) { - throw new IllegalStateException( - "Unused classes (%s) found with a @%s annotation.".formatted(String.join(", ", unusedSolutionClassList), - PlanningSolution.class.getSimpleName())); + }); + var solutionClassInstance = planningSolutionAnnotationInstanceCollection.iterator().next(); + var solutionClassInfo = solutionClassInstance.target().asClass(); + var visited = new HashSet(); + for (var annotatedMember : membersToGeneratedAccessorsForCollection) { + ClassInfo classInfo = null; + String memberName = null; + if (!visited.add(annotatedMember.target())) { + continue; } - - planningSolutionAnnotationInstanceCollection.forEach(planningSolutionAnnotationInstance -> { - var autoDiscoverMemberType = planningSolutionAnnotationInstance.values().stream() - .filter(v -> v.name().equals("autoDiscoverMemberType")) - .findFirst() - .map(AnnotationValue::asEnum) - .map(AutoDiscoverMemberType::valueOf) - .orElse(AutoDiscoverMemberType.NONE); - - if (autoDiscoverMemberType != AutoDiscoverMemberType.NONE) { - throw new UnsupportedOperationException(""" - Auto-discovery of members using %s is not supported under Quarkus. - Remove the autoDiscoverMemberType property from the @%s annotation - and explicitly annotate the fields or getters with annotations such as @%s, @%s or @%s.""" - .strip() - .formatted( - AutoDiscoverMemberType.class.getSimpleName(), - PlanningSolution.class.getSimpleName(), - PlanningScore.class.getSimpleName(), - PlanningEntityCollectionProperty.class.getSimpleName(), - ProblemFactCollectionProperty.class.getSimpleName())); + switch (annotatedMember.target().kind()) { + case FIELD -> { + var fieldInfo = annotatedMember.target().asField(); + classInfo = fieldInfo.declaringClass(); + memberName = fieldInfo.name(); + buildFieldAccessor(annotatedMember, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, + classInfo, fieldInfo, transformers); } - }); - var solutionClassInstance = planningSolutionAnnotationInstanceCollection.iterator().next(); - var solutionClassInfo = solutionClassInstance.target().asClass(); - var visited = new HashSet(); - for (var annotatedMember : membersToGeneratedAccessorsForCollection) { - ClassInfo classInfo = null; - String memberName = null; - if (!visited.add(annotatedMember.target())) { - continue; + case METHOD -> { + var methodInfo = annotatedMember.target().asMethod(); + classInfo = methodInfo.declaringClass(); + memberName = methodInfo.name(); + buildMethodAccessor(annotatedMember, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, + classInfo, methodInfo, + AccessorInfo.of((annotatedMember.name().equals(DotNames.VALUE_RANGE_PROVIDER)) + ? MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER + : MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD), + transformers); } - switch (annotatedMember.target().kind()) { - case FIELD -> { - var fieldInfo = annotatedMember.target().asField(); - classInfo = fieldInfo.declaringClass(); - memberName = fieldInfo.name(); - buildFieldAccessor(annotatedMember, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, - classInfo, fieldInfo, transformers); - } - case METHOD -> { - var methodInfo = annotatedMember.target().asMethod(); - classInfo = methodInfo.declaringClass(); - memberName = methodInfo.name(); - buildMethodAccessor(annotatedMember, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, - classInfo, methodInfo, - AccessorInfo.of(true, annotatedMember.name().equals(DotNames.VALUE_RANGE_PROVIDER)), - transformers); - } - default -> throw new IllegalStateException( - "The member (%s) is not on a field or method.".formatted(annotatedMember)); + default -> throw new IllegalStateException( + "The member (%s) is not on a field or method.".formatted(annotatedMember)); + } + if (annotatedMember.name().equals(DotNames.CASCADING_UPDATE_SHADOW_VARIABLE)) { + // The source method name also must be included + // targetMethodName is a required field and is always present + var targetMethodName = annotatedMember.value("targetMethodName").asString(); + var methodInfo = classInfo.method(targetMethodName); + buildMethodAccessor(null, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, classInfo, + methodInfo, AccessorInfo.of(MemberAccessorFactory.MemberAccessorType.VOID_METHOD), transformers); + } else if (annotatedMember.name().equals(DotNames.SHADOW_VARIABLE) + && annotatedMember.value("supplierName") != null) { + // The source method name also must be included + var targetMethodName = annotatedMember.value("supplierName") + .asString(); + var methodInfo = classInfo.method(targetMethodName); + if (methodInfo == null) { + // Retry with the solution class + var solutionType = Type.create(solutionClassInfo.name(), Type.Kind.CLASS); + methodInfo = classInfo.method(targetMethodName, solutionType); } - if (annotatedMember.name().equals(DotNames.CASCADING_UPDATE_SHADOW_VARIABLE)) { - // The source method name also must be included - // targetMethodName is a required field and is always present - var targetMethodName = annotatedMember.value("targetMethodName").asString(); - var methodInfo = classInfo.method(targetMethodName); - buildMethodAccessor(null, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, classInfo, - methodInfo, AccessorInfo.of(false, false), transformers); - } else if (annotatedMember.name().equals(DotNames.SHADOW_VARIABLE) - && annotatedMember.value("supplierName") != null) { - // The source method name also must be included - var targetMethodName = annotatedMember.value("supplierName") - .asString(); - var methodInfo = classInfo.method(targetMethodName); - if (methodInfo == null) { - // Retry with the solution class - var solutionType = Type.create(solutionClassInfo.name(), Type.Kind.CLASS); - methodInfo = classInfo.method(targetMethodName, solutionType); - } - if (methodInfo == null) { - throw new IllegalArgumentException( - """ - @%s (%s) defines a supplierName (%s) that does not exist inside its declaring class (%s). - Maybe you included a parameter which is not a planning solution (%s)? - Maybe you misspelled the supplierName name?""" - .formatted(ShadowVariable.class.getSimpleName(), memberName, targetMethodName, - classInfo.name().toString(), solutionClassInfo.name().toString())); - } - buildMethodAccessor(annotatedMember, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, - classInfo, methodInfo, AccessorInfo.of(true, !methodInfo.parameterTypes().isEmpty()), transformers); + if (methodInfo == null) { + throw new IllegalArgumentException( + """ + @%s (%s) defines a supplierName (%s) that does not exist inside its declaring class (%s). + Maybe you included a parameter which is not a planning solution (%s)? + Maybe you misspelled the supplierName name?""" + .formatted(ShadowVariable.class.getSimpleName(), memberName, targetMethodName, + classInfo.name().toString(), solutionClassInfo.name().toString())); } + buildMethodAccessor(annotatedMember, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, + classInfo, methodInfo, + AccessorInfo + .of(MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER), + transformers); } - // The ConstraintWeightOverrides field is not annotated, but it needs a member accessor - var constraintFieldInfo = solutionClassInfo.fields().stream() - .filter(f -> f.type().name().equals(DotNames.CONSTRAINT_WEIGHT_OVERRIDES)) + } + // The ConstraintWeightOverrides field is not annotated, but it needs a member accessor + var constraintFieldInfo = solutionClassInfo.fields().stream() + .filter(f -> f.type().name().equals(DotNames.CONSTRAINT_WEIGHT_OVERRIDES)) + .findFirst() + .orElse(null); + if (constraintFieldInfo != null) { + // Prefer method to field + var solutionClass = convertClassInfoToClass(solutionClassInfo); + var constraintMethod = + ReflectionHelper.getGetterMethod(solutionClass, constraintFieldInfo.name()); + var constraintMethodInfo = solutionClassInfo.methods().stream() + .filter(m -> constraintMethod != null && m.name().equals(constraintMethod.getName()) + && m.parametersCount() == 0) .findFirst() .orElse(null); - if (constraintFieldInfo != null) { - // Prefer method to field - var solutionClass = convertClassInfoToClass(solutionClassInfo); - var constraintMethod = - ReflectionHelper.getGetterMethod(solutionClass, constraintFieldInfo.name()); - var constraintMethodInfo = solutionClassInfo.methods().stream() - .filter(m -> constraintMethod != null && m.name().equals(constraintMethod.getName()) - && m.parametersCount() == 0) - .findFirst() - .orElse(null); - if (constraintMethodInfo != null) { - buildMethodAccessor(solutionClassInstance, generatedMemberAccessorsClassNameSet, entityEnhancer, - classOutput, solutionClassInfo, constraintMethodInfo, AccessorInfo.withReturnValueAndNoArguments(), - transformers); - } else { - buildFieldAccessor(solutionClassInstance, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, - solutionClassInfo, constraintFieldInfo, transformers); - } + if (constraintMethodInfo != null) { + buildMethodAccessor(solutionClassInstance, generatedMemberAccessorsClassNameSet, entityEnhancer, + classOutput, solutionClassInfo, constraintMethodInfo, AccessorInfo.withReturnValueAndNoArguments(), + transformers); + } else { + buildFieldAccessor(solutionClassInstance, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, + solutionClassInfo, constraintFieldInfo, transformers); } - // Using REFLECTION domain access type so Timefold doesn't try to generate GIZMO code - solverConfigMap.values().forEach(c -> { - var solutionDescriptor = SolutionDescriptor.buildSolutionDescriptor( - c.getEnablePreviewFeatureSet(), DomainAccessType.REFLECTION, - c.getSolutionClass(), null, null, c.getEntityClassList()); - gizmoSolutionClonerClassNameSet - .add(entityEnhancer.generateSolutionCloner(solutionDescriptor, classOutput, indexView, transformers)); - }); } + // Using REFLECTION domain access type so Timefold doesn't try to generate GIZMO code + solverConfigMap.values().forEach(c -> { + var solutionDescriptor = SolutionDescriptor.buildSolutionDescriptor( + c.getEnablePreviewFeatureSet(), DomainAccessType.REFLECTION, + c.getSolutionClass(), null, null, c.getEntityClassList()); + gizmoSolutionClonerClassNameSet + .add(entityEnhancer.generateSolutionCloner(solutionDescriptor, classOutput, indexView, transformers)); + }); entityEnhancer.generateGizmoBeanFactory(beanClassOutput, reflectiveClassSet, transformers); return new GeneratedGizmoClasses(generatedMemberAccessorsClassNameSet, gizmoSolutionClonerClassNameSet); @@ -1210,19 +1200,6 @@ private static void buildMethodAccessor(AnnotationInstance annotatedMember, } } - private void assertSolverDomainAccessType(Map solverConfigMap) { - // All solver must use the same domain access type - if (solverConfigMap.values().stream().map(SolverConfig::getDomainAccessType).distinct().count() > 1) { - throw new ConfigurationException( - """ - The domain access type must be unique across all Solver configurations. - %s""".formatted(solverConfigMap.entrySet().stream() - .map(e -> format("quarkus.timefold.\"%s\".domain-access-type=%s", - e.getKey(), e.getValue().getDomainAccessType())) - .collect(Collectors.joining("\n")))); - } - } - private boolean shouldIgnoreMember(AnnotationInstance annotationInstance) { return switch (annotationInstance.target().kind()) { case FIELD -> (annotationInstance.target().asField().flags() & Modifier.STATIC) != 0; diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java index 1d0858db5f6..1984e2fbf2a 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java @@ -3,7 +3,6 @@ import java.util.Optional; import java.util.Set; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.quarkus.config.SolverRuntimeConfig; @@ -27,14 +26,6 @@ public interface SolverBuildTimeConfig { // which generates the constructor of classes used by Quarkus Optional solverConfigXml(); - /** - * Determines how to access the fields and methods of domain classes. - * Defaults to {@link DomainAccessType#GIZMO}. - */ - // Build time - GIZMO classes are only generated if at least one solver - // has domain access type GIZMO - Optional domainAccessType(); - /** * Enable the Nearby Selection quick configuration. */ diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNearbySolverYamlTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNearbySolverYamlTest.java index ced3351578e..613c8635adc 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNearbySolverYamlTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNearbySolverYamlTest.java @@ -9,7 +9,6 @@ import jakarta.inject.Inject; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.solver.EnvironmentMode; @@ -46,7 +45,6 @@ void solverProperties() { assertNotNull(solverConfig.getNearbyDistanceMeterClass()); assertTrue(solverConfig.getDaemon()); assertEquals("2", solverConfig.getMoveThreadCount()); - assertEquals(DomainAccessType.REFLECTION, solverConfig.getDomainAccessType()); assertNotNull(solverFactory); } diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverPropertiesTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverPropertiesTest.java index 75e27272a9c..a281074f1f7 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverPropertiesTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverPropertiesTest.java @@ -8,7 +8,6 @@ import jakarta.inject.Inject; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.solver.EnvironmentMode; @@ -55,7 +54,6 @@ void solverProperties() { assertEquals(EnvironmentMode.FULL_ASSERT, solverConfig.getEnvironmentMode()); assertTrue(solverConfig.getDaemon()); assertEquals("2", solverConfig.getMoveThreadCount()); - assertEquals(DomainAccessType.REFLECTION, solverConfig.getDomainAccessType()); assertNotNull(solverConfig.getNearbyDistanceMeterClass()); assertNotNull(solverFactory); } diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverResourcesTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverResourcesTest.java index 8ac36d50a66..8af88f347b5 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverResourcesTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverResourcesTest.java @@ -7,7 +7,6 @@ import jakarta.inject.Inject; import jakarta.inject.Named; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.score.BendableBigDecimalScore; import ai.timefold.solver.core.api.score.BendableScore; import ai.timefold.solver.core.api.score.HardMediumSoftBigDecimalScore; @@ -89,7 +88,6 @@ void solverProperties() { assertThat((Object) solverConfig.getEnvironmentMode()).isEqualTo(EnvironmentMode.FULL_ASSERT); assertThat(solverConfig.getNearbyDistanceMeterClass()).isNull(); assertThat(solverConfig.getDaemon()).isTrue(); - assertThat(solverConfig.getDomainAccessType()).isEqualTo(DomainAccessType.REFLECTION); assertThat(solver1Factory).isNotNull(); assertThat(solverConfig.getTerminationConfig().getSpentLimit()).isEqualTo(Duration.ofHours(4)); assertThat(solverConfig.getTerminationConfig().getUnimprovedSpentLimit()).isEqualTo(Duration.ofHours(5)); diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverYamlTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverYamlTest.java index 113dd5b600d..4065c7dadd4 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverYamlTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverYamlTest.java @@ -8,7 +8,6 @@ import jakarta.inject.Inject; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.solver.EnvironmentMode; @@ -43,7 +42,6 @@ void solverProperties() { assertEquals(EnvironmentMode.FULL_ASSERT, solverConfig.getEnvironmentMode()); assertTrue(solverConfig.getDaemon()); assertEquals("2", solverConfig.getMoveThreadCount()); - assertEquals(DomainAccessType.REFLECTION, solverConfig.getDomainAccessType()); assertNotNull(solverFactory); } diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLDefaultTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLDefaultTest.java index 85f5d2e00b8..8110b072734 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLDefaultTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLDefaultTest.java @@ -7,7 +7,6 @@ import jakarta.inject.Inject; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.quarkus.testdomain.normal.TestdataQuarkusConstraintProvider; @@ -39,7 +38,6 @@ class TimefoldProcessorXMLDefaultTest { void solverConfigXml_default() { assertNotNull(solverConfig); assertEquals(TestdataQuarkusSolution.class, solverConfig.getSolutionClass()); - assertEquals(DomainAccessType.GIZMO, solverConfig.getDomainAccessType()); assertEquals(Collections.singletonList(TestdataQuarkusEntity.class), solverConfig.getEntityClassList()); assertEquals(TestdataQuarkusConstraintProvider.class, solverConfig.getScoreDirectorFactoryConfig().getConstraintProviderClass()); diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLNearbyPropertyTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLNearbyPropertyTest.java index 68e34b976e2..869b63a3d1a 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLNearbyPropertyTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLNearbyPropertyTest.java @@ -7,7 +7,6 @@ import jakarta.inject.Inject; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.quarkus.testdomain.dummy.DummyDistanceMeter; @@ -41,7 +40,6 @@ class TimefoldProcessorXMLNearbyPropertyTest { void solverConfigXml_property() { assertNotNull(solverConfig); assertNotNull(solverConfig.getNearbyDistanceMeterClass()); - assertEquals(DomainAccessType.GIZMO, solverConfig.getDomainAccessType()); assertEquals(TestdataQuarkusSolution.class, solverConfig.getSolutionClass()); assertEquals(Collections.singletonList(TestdataQuarkusEntity.class), solverConfig.getEntityClassList()); assertEquals(TestdataQuarkusConstraintProvider.class, diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLNoneTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLNoneTest.java index 06159003615..29b057bf494 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLNoneTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLNoneTest.java @@ -8,7 +8,6 @@ import jakarta.inject.Inject; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.quarkus.testdomain.normal.TestdataQuarkusConstraintProvider; @@ -39,7 +38,6 @@ class TimefoldProcessorXMLNoneTest { void solverConfigXml_default() { assertNotNull(solverConfig); assertEquals(TestdataQuarkusSolution.class, solverConfig.getSolutionClass()); - assertEquals(DomainAccessType.GIZMO, solverConfig.getDomainAccessType()); assertEquals(Collections.singletonList(TestdataQuarkusEntity.class), solverConfig.getEntityClassList()); assertEquals(TestdataQuarkusConstraintProvider.class, solverConfig.getScoreDirectorFactoryConfig().getConstraintProviderClass()); diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLPropertyTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLPropertyTest.java index a3f0e61c851..5d962e84bce 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLPropertyTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorXMLPropertyTest.java @@ -7,7 +7,6 @@ import jakarta.inject.Inject; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.quarkus.testdomain.normal.TestdataQuarkusConstraintProvider; @@ -39,7 +38,6 @@ class TimefoldProcessorXMLPropertyTest { @Test void solverConfigXml_property() { assertNotNull(solverConfig); - assertEquals(DomainAccessType.GIZMO, solverConfig.getDomainAccessType()); assertEquals(TestdataQuarkusSolution.class, solverConfig.getSolutionClass()); assertEquals(Collections.singletonList(TestdataQuarkusEntity.class), solverConfig.getEntityClassList()); assertEquals(TestdataQuarkusConstraintProvider.class, diff --git a/quarkus-integration/quarkus/deployment/src/test/resources/ai/timefold/solver/quarkus/single-solver/application-nearby.yaml b/quarkus-integration/quarkus/deployment/src/test/resources/ai/timefold/solver/quarkus/single-solver/application-nearby.yaml index 0d8112f81c6..536b5f8d3a7 100644 --- a/quarkus-integration/quarkus/deployment/src/test/resources/ai/timefold/solver/quarkus/single-solver/application-nearby.yaml +++ b/quarkus-integration/quarkus/deployment/src/test/resources/ai/timefold/solver/quarkus/single-solver/application-nearby.yaml @@ -4,7 +4,6 @@ quarkus: environment-mode: FULL_ASSERT daemon: true move-thread-count: 2 - domain-access-type: REFLECTION nearby-distance-meter-class: ai.timefold.solver.quarkus.testdomain.dummy.DummyDistanceMeter termination: spent-limit: 4h diff --git a/quarkus-integration/quarkus/deployment/src/test/resources/ai/timefold/solver/quarkus/single-solver/application.yaml b/quarkus-integration/quarkus/deployment/src/test/resources/ai/timefold/solver/quarkus/single-solver/application.yaml index 5cfe19b3242..5bcae6babc7 100644 --- a/quarkus-integration/quarkus/deployment/src/test/resources/ai/timefold/solver/quarkus/single-solver/application.yaml +++ b/quarkus-integration/quarkus/deployment/src/test/resources/ai/timefold/solver/quarkus/single-solver/application.yaml @@ -4,7 +4,6 @@ quarkus: environment-mode: FULL_ASSERT daemon: true move-thread-count: 2 - domain-access-type: REFLECTION termination: spent-limit: 4h unimproved-spent-limit: 5h diff --git a/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUIMultipleSolversTest.java b/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUIMultipleSolversTest.java index ad0ed8f785a..2afefac4092 100644 --- a/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUIMultipleSolversTest.java +++ b/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUIMultipleSolversTest.java @@ -71,7 +71,6 @@ private void assertSolverConfigPage(String solverConfig) { + " " + TestdataStringLengthShadowSolution.class.getCanonicalName() + "\n" + " " + TestdataStringLengthShadowEntity.class.getCanonicalName() + "\n" - + " GIZMO\n" + " \n" + " " + TestdataStringLengthConstraintProvider.class.getCanonicalName() + "\n" diff --git a/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUITest.java b/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUITest.java index 6efb9a1bb8e..e6c6a2257db 100644 --- a/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUITest.java +++ b/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUITest.java @@ -79,7 +79,6 @@ void testSolverConfigPage() throws Exception { + " " + TestdataStringLengthShadowSolution.class.getCanonicalName() + "\n" + " " + TestdataStringLengthShadowEntity.class.getCanonicalName() + "\n" - + " GIZMO\n" + " \n" + " " + TestdataStringLengthConstraintProvider.class.getCanonicalName() + "\n" diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java index e88b533c104..e2691faf380 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java @@ -23,12 +23,8 @@ public TimefoldSolverAotContribution(Map solverConfigMap) */ private static void registerType(ReflectionHints reflectionHints, Class type) { reflectionHints.registerType(type, - MemberCategory.INTROSPECT_PUBLIC_METHODS, - MemberCategory.INTROSPECT_DECLARED_METHODS, - MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, - MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS, - MemberCategory.PUBLIC_FIELDS, - MemberCategory.DECLARED_FIELDS, + MemberCategory.ACCESS_PUBLIC_FIELDS, + MemberCategory.ACCESS_DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS, diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java index 2f4b5b411d9..026efca83f9 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java @@ -277,9 +277,6 @@ private void applyScoreDirectorFactoryProperties(IncludeAbstractClassesEntitySca if (solverProperties.getEnvironmentMode() != null) { solverConfig.setEnvironmentMode(solverProperties.getEnvironmentMode()); } - if (solverProperties.getDomainAccessType() != null) { - solverConfig.setDomainAccessType(solverProperties.getDomainAccessType()); - } if (solverProperties.getEnabledPreviewFeatures() != null) { solverConfig.setEnablePreviewFeatureSet(new HashSet<>(solverProperties.getEnabledPreviewFeatures())); } diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java index 22e56de6377..69d2e33f74a 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java @@ -4,7 +4,6 @@ import java.util.Map; import java.util.TreeSet; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; @@ -41,15 +40,6 @@ public class SolverProperties { */ private String moveThreadCount; - /** - * Determines how to access the fields and methods of domain classes. - * Defaults to REFLECTION. - *

- * To use GIZMO, io.quarkus.gizmo:gizmo must be in your classpath, - * and all planning annotations must be on public members. - */ - private DomainAccessType domainAccessType; - private List enabledPreviewFeatures; /** @@ -115,14 +105,6 @@ public void setMoveThreadCount(String moveThreadCount) { this.moveThreadCount = moveThreadCount; } - public DomainAccessType getDomainAccessType() { - return domainAccessType; - } - - public void setDomainAccessType(DomainAccessType domainAccessType) { - this.domainAccessType = domainAccessType; - } - public List getEnabledPreviewFeatures() { return enabledPreviewFeatures; } diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java index a5d5fcaed83..a5d6b4b53e6 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java @@ -10,7 +10,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; @@ -21,8 +20,6 @@ public enum SolverProperty { value -> EnvironmentMode.valueOf(value.toString())), DAEMON("daemon", SolverProperties::setDaemon, value -> Boolean.valueOf(value.toString())), MOVE_THREAD_COUNT("move-thread-count", SolverProperties::setMoveThreadCount, Object::toString), - DOMAIN_ACCESS_TYPE("domain-access-type", SolverProperties::setDomainAccessType, - value -> DomainAccessType.valueOf(value.toString())), ENABLED_PREVIEW_FEATURES("enabled-preview-features", SolverProperties::setEnabledPreviewFeatures, value -> Arrays.stream(value.toString().split(",")).map(PreviewFeature::valueOf).toList()), NEARBY_DISTANCE_METER_CLASS("nearby-distance-meter-class", SolverProperties::setNearbyDistanceMeterClass, diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverGizmoAutoConfigurationTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverGizmoAutoConfigurationTest.java deleted file mode 100644 index a9c9ac99767..00000000000 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverGizmoAutoConfigurationTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package ai.timefold.solver.spring.boot.autoconfigure; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; - -import ai.timefold.solver.core.api.domain.common.DomainAccessType; -import ai.timefold.solver.core.api.solver.SolverFactory; -import ai.timefold.solver.core.api.solver.SolverManager; -import ai.timefold.solver.core.config.solver.SolverConfig; -import ai.timefold.solver.spring.boot.autoconfigure.basic.domain.TestdataSpringSolution; -import ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties; -import ai.timefold.solver.spring.boot.autoconfigure.gizmo.GizmoSpringTestConfiguration; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.parallel.Execution; -import org.junit.jupiter.api.parallel.ExecutionMode; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.core.io.ClassPathResource; -import org.springframework.test.context.TestExecutionListeners; - -@TestExecutionListeners -@Execution(ExecutionMode.CONCURRENT) -class TimefoldSolverGizmoAutoConfigurationTest { - - private final ApplicationContextRunner gizmoContextRunner; - private final FilteredClassLoader noGizmoFilteredClassLoader; - - public TimefoldSolverGizmoAutoConfigurationTest() { - gizmoContextRunner = new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) - .withUserConfiguration(GizmoSpringTestConfiguration.class); - noGizmoFilteredClassLoader = new FilteredClassLoader(FilteredClassLoader.PackageFilter.of("io.quarkus.gizmo2"), - FilteredClassLoader.ClassPathResourceFilter.of( - new ClassPathResource(TimefoldProperties.DEFAULT_SOLVER_CONFIG_URL))); - } - - @Test - void solverProperties() { - gizmoContextRunner - .withPropertyValues("timefold.solver.domain-access-type=GIZMO") - .run(context -> { - var solverConfig = context.getBean(SolverConfig.class); - assertThat(solverConfig.getDomainAccessType()).isEqualTo(DomainAccessType.GIZMO); - assertThat(context.getBean(SolverFactory.class)).isNotNull(); - }); - gizmoContextRunner - .withPropertyValues("timefold.solver.solver1.domain-access-type=GIZMO") - .withPropertyValues("timefold.solver.solver2.domain-access-type=REFLECTION") - .run(context -> { - var solver1 = - (SolverManager) context.getBean("solver1"); - var solver2 = - (SolverManager) context.getBean("solver2"); - assertThat(solver1).isNotNull(); - assertThat(solver2).isNotNull(); - }); - } - - @Test - void gizmoThrowsIfGizmoNotPresent() { - assertThatCode(() -> gizmoContextRunner - .withClassLoader(noGizmoFilteredClassLoader) - .withPropertyValues( - "timefold.solver-config-xml=ai/timefold/solver/spring/boot/autoconfigure/gizmoSpringBootSolverConfig.xml") - .run(context -> context.getBean(SolverFactory.class))) - .hasRootCauseMessage("When using the domainAccessType (" + - DomainAccessType.GIZMO + - ") the classpath or modulepath must contain io.quarkus.gizmo:gizmo2.\n" + - "Maybe add a dependency to io.quarkus.gizmo:gizmo2."); - } - -} diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverWithSolverConfigXmlAutoConfigurationTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverWithSolverConfigXmlAutoConfigurationTest.java index 427718eb9d5..ff7815cb2bd 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverWithSolverConfigXmlAutoConfigurationTest.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverWithSolverConfigXmlAutoConfigurationTest.java @@ -9,7 +9,6 @@ import java.time.Duration; import java.util.Collections; -import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.solver.SolutionManager; import ai.timefold.solver.core.api.solver.SolverFactory; @@ -233,7 +232,6 @@ void solverWithYaml() { assertEquals(EnvironmentMode.FULL_ASSERT, solverConfig.getEnvironmentMode()); assertTrue(solverConfig.getDaemon()); assertEquals("2", solverConfig.getMoveThreadCount()); - assertEquals(DomainAccessType.REFLECTION, solverConfig.getDomainAccessType()); assertEquals(Duration.ofHours(4), solverConfig.getTerminationConfig().getSpentLimit()); assertEquals(Duration.ofHours(5), solverConfig.getTerminationConfig().getUnimprovedSpentLimit()); assertEquals(SimpleScore.of(0).toString(), solverConfig.getTerminationConfig().getBestScoreLimit()); diff --git a/spring-integration/spring-boot-autoconfigure/src/test/resources/ai/timefold/solver/spring/boot/autoconfigure/single-solver/application.yaml b/spring-integration/spring-boot-autoconfigure/src/test/resources/ai/timefold/solver/spring/boot/autoconfigure/single-solver/application.yaml index 93b6cce396e..e6e2f349b0b 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/resources/ai/timefold/solver/spring/boot/autoconfigure/single-solver/application.yaml +++ b/spring-integration/spring-boot-autoconfigure/src/test/resources/ai/timefold/solver/spring/boot/autoconfigure/single-solver/application.yaml @@ -4,7 +4,6 @@ timefold: environment-mode: FULL_ASSERT daemon: true move-thread-count: 2 - domain-access-type: REFLECTION nearby-distance-meter-class: ai.timefold.solver.spring.boot.autoconfigure.dummy.DummyDistanceMeter termination: spent-limit: 4h diff --git a/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml b/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml index 15b66f8bdcd..136ebe5aeed 100644 --- a/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml +++ b/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml @@ -14,7 +14,6 @@ java.lang.Object java.lang.Object - GIZMO java.lang.Object diff --git a/tools/benchmark/src/main/resources/benchmark.xsd b/tools/benchmark/src/main/resources/benchmark.xsd index 16219c25079..79a4242b0a5 100644 --- a/tools/benchmark/src/main/resources/benchmark.xsd +++ b/tools/benchmark/src/main/resources/benchmark.xsd @@ -353,9 +353,6 @@ - - - @@ -2573,24 +2570,6 @@ - - - - - - - - - - - - - - - - - - From ceaca30a4d6a790cc4164d6d8cc4ca57fb195e46 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 17 Feb 2026 18:13:59 -0500 Subject: [PATCH 2/3] chore: review comments --- .../solver/core/config/util/ConfigUtils.java | 2 +- .../impl/domain/common/DomainAccessType.java | 4 +- .../accessor/MemberAccessorFactory.java | 47 +-- .../common/accessor/MemberAccessorType.java | 23 ++ .../accessor/MemberAccessorValidator.java | 226 +++++++++++++ .../ReflectionBeanPropertyMemberAccessor.java | 53 --- ...eflectionMethodExtendedMemberAccessor.java | 6 +- .../ReflectionMethodMemberAccessor.java | 17 - .../common/accessor/gizmo/AccessorInfo.java | 14 +- .../accessor/gizmo/GizmoClassLoader.java | 60 +++- .../accessor/gizmo/GizmoFieldHandler.java | 19 +- .../gizmo/GizmoMemberAccessorFactory.java | 54 --- .../gizmo/GizmoMemberAccessorImplementor.java | 128 +------ .../accessor/gizmo/GizmoMemberDescriptor.java | 4 +- .../accessor/gizmo/GizmoMemberHandler.java | 5 +- .../accessor/gizmo/GizmoSupportStatus.java | 3 +- .../entity/descriptor/EntityDescriptor.java | 6 +- .../impl/domain/policy/DescriptorPolicy.java | 4 +- ...verridesBasedConstraintWeightSupplier.java | 3 +- .../descriptor/SolutionDescriptor.java | 10 +- ...scadingUpdateShadowVariableDescriptor.java | 4 +- .../DeclarativeShadowVariableDescriptor.java | 6 +- .../declarative/RootVariableSource.java | 3 +- .../accessor/MemberAccessorFactoryTest.java | 22 +- .../accessor/MemberAccessorValidatorTest.java | 313 ++++++++++++++++++ ...lectionBeanPropertyMemberAccessorTest.java | 40 --- ...ctionMethodExtendedMemberAccessorTest.java | 68 +--- .../GizmoMemberAccessorImplementorTest.java | 66 +--- .../domain/lookup/AbstractLookupTest.java | 2 +- .../descriptor/SolutionDescriptorTest.java | 2 +- .../TestdataUnknownFactTypeSolution.java | 4 + docs/TODO.md | 3 +- .../pages/integration/config-properties.adoc | 12 - .../ROOT/pages/integration/integration.adoc | 7 +- .../using-timefold-solver/configuration.adoc | 27 -- .../quarkus/deployment/TimefoldProcessor.java | 25 +- 36 files changed, 715 insertions(+), 577 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorType.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorValidator.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorValidatorTest.java diff --git a/core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java b/core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java index 7c71f8634da..14b32c94bc5 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java +++ b/core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java @@ -1,6 +1,6 @@ package ai.timefold.solver.core.config.util; -import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD; +import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType.FIELD_OR_READ_METHOD; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/DomainAccessType.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/DomainAccessType.java index a0255cddaf6..c2827850bbb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/DomainAccessType.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/DomainAccessType.java @@ -20,7 +20,7 @@ public enum DomainAccessType { * When used in a modulepath, the module must be open. * When used in GraalVM, the domain must be open for reflection. */ - REFLECTION, + FORCE_REFLECTION, /** * Use Gizmo generated bytecode to access members (fields and methods) to avoid reflection @@ -28,5 +28,5 @@ public enum DomainAccessType { *

* This is the default when the application is run inside a JVM and not a native image. */ - GIZMO + FORCE_GIZMO } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactory.java index 7462886dab8..f916cdf6fba 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactory.java @@ -37,7 +37,7 @@ public final class MemberAccessorFactory { * @param member never null, method or field to access * @param memberAccessorType never null * @param domainAccessType never null - * @param classLoader null or {@link GizmoClassLoader} if domainAccessType is {@link DomainAccessType#GIZMO}. + * @param classLoader null or {@link GizmoClassLoader} if domainAccessType is {@link DomainAccessType#FORCE_GIZMO}. * @return never null, new instance of the member accessor */ private static MemberAccessor buildMemberAccessor(Member member, MemberAccessorType memberAccessorType, @@ -52,19 +52,20 @@ private static MemberAccessor buildMemberAccessor(Member member, MemberAccessorT * @param memberAccessorType never null * @param annotationClass the annotation the member was annotated with (used for error reporting) * @param domainAccessType never null - * @param classLoader null or {@link GizmoClassLoader} if domainAccessType is {@link DomainAccessType#GIZMO}. + * @param classLoader null or {@link GizmoClassLoader} if domainAccessType is {@link DomainAccessType#FORCE_GIZMO}. * @return never null, new instance of the member accessor */ static MemberAccessor buildMemberAccessor(Member member, MemberAccessorType memberAccessorType, @Nullable Class annotationClass, DomainAccessType domainAccessType, ClassLoader classLoader) { + MemberAccessorValidator.verifyIsValidMember(annotationClass, member, memberAccessorType); return switch (domainAccessType) { case AUTO -> throw new IllegalStateException( "Impossible state: called with %s (AUTO) instead of a resolved domain access type" .formatted(DomainAccessType.class.getSimpleName())); - case GIZMO -> GizmoMemberAccessorFactory.buildGizmoMemberAccessor(member, annotationClass, + case FORCE_GIZMO -> GizmoMemberAccessorFactory.buildGizmoMemberAccessor(member, annotationClass, AccessorInfo.of(memberAccessorType), (GizmoClassLoader) Objects.requireNonNull(classLoader)); - case REFLECTION -> buildReflectiveMemberAccessor(member, memberAccessorType, annotationClass); + case FORCE_REFLECTION -> buildReflectiveMemberAccessor(member, memberAccessorType, annotationClass); }; } @@ -135,7 +136,7 @@ private static MemberAccessor buildReflectiveMemberAccessor(Member member, Membe memberAccessor = new ReflectionBeanPropertyMemberAccessor(method, annotatedElement, getterOnly); break; case VOID_METHOD: - memberAccessor = new ReflectionMethodMemberAccessor(method, false, false); + memberAccessor = new ReflectionMethodMemberAccessor(method); break; default: throw new IllegalStateException("The memberAccessorType (%s) is not implemented." @@ -172,7 +173,9 @@ public MemberAccessorFactory() { /** * Prefills the member accessor cache. * - * @param memberAccessorMap key is the fully qualified member name + * @param memberAccessorMap key is the fully qualified member name, value is a pregenerated {@link MemberAccessor}. + * Used by Quarkus since the {@link MemberAccessor} are generated at build time. + * If null, it is treated as an empty map. */ public MemberAccessorFactory(@Nullable Map memberAccessorMap) { // The MemberAccessorFactory may be accessed, and this cache both read and updated, by multiple threads. @@ -180,10 +183,9 @@ public MemberAccessorFactory(@Nullable Map memberAccesso memberAccessorMap == null ? new ConcurrentHashMap<>() : new ConcurrentHashMap<>(memberAccessorMap); // If the memberAccessorMap is not empty, we are in Quarkus using pregenerated member accessors this.isGizmoSupported = - (memberAccessorMap != null && !memberAccessorMap.isEmpty()) || GizmoMemberAccessorFactory.isGizmoSupported( - gizmoClassLoader); - LOGGER.trace("Using domain access type {} for member accessors", - isGizmoSupported ? DomainAccessType.GIZMO : DomainAccessType.REFLECTION); + (memberAccessorMap != null && !memberAccessorMap.isEmpty()) || gizmoClassLoader.isGizmoSupported(); + LOGGER.trace("Using domain access type {} for member accessors.", + isGizmoSupported ? DomainAccessType.FORCE_GIZMO : DomainAccessType.FORCE_REFLECTION); } /** @@ -199,7 +201,7 @@ public MemberAccessor buildAndCacheMemberAccessor(Member member, MemberAccessorT @Nullable Class annotationClass, DomainAccessType domainAccessType) { String generatedClassName = GizmoMemberAccessorFactory.getGeneratedClassName(member); if (domainAccessType == DomainAccessType.AUTO) { - domainAccessType = isGizmoSupported ? DomainAccessType.GIZMO : DomainAccessType.REFLECTION; + domainAccessType = isGizmoSupported ? DomainAccessType.FORCE_GIZMO : DomainAccessType.FORCE_REFLECTION; } var finalDomainAccessType = domainAccessType; @@ -221,7 +223,7 @@ public MemberAccessor buildAndCacheMemberAccessor(Member member, MemberAccessorT DomainAccessType domainAccessType) { String generatedClassName = GizmoMemberAccessorFactory.getGeneratedClassName(member); if (domainAccessType == DomainAccessType.AUTO) { - domainAccessType = isGizmoSupported ? DomainAccessType.GIZMO : DomainAccessType.REFLECTION; + domainAccessType = isGizmoSupported ? DomainAccessType.FORCE_GIZMO : DomainAccessType.FORCE_REFLECTION; } var finalDomainAccessType = domainAccessType; @@ -234,25 +236,4 @@ public GizmoClassLoader getGizmoClassLoader() { return gizmoClassLoader; } - public enum MemberAccessorType { - FIELD_OR_READ_METHOD, - FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, - FIELD_OR_GETTER_METHOD, - FIELD_OR_GETTER_METHOD_WITH_SETTER(true), - VOID_METHOD; - - private final boolean setterRequired; - - MemberAccessorType() { - setterRequired = false; - } - - MemberAccessorType(boolean setterRequired) { - this.setterRequired = setterRequired; - } - - public boolean isSetterRequired() { - return setterRequired; - } - } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorType.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorType.java new file mode 100644 index 00000000000..f5d5324001a --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorType.java @@ -0,0 +1,23 @@ +package ai.timefold.solver.core.impl.domain.common.accessor; + +public enum MemberAccessorType { + FIELD_OR_READ_METHOD, + FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + FIELD_OR_GETTER_METHOD, + FIELD_OR_GETTER_METHOD_WITH_SETTER(true), + VOID_METHOD; + + private final boolean setterRequired; + + MemberAccessorType() { + setterRequired = false; + } + + MemberAccessorType(boolean setterRequired) { + this.setterRequired = setterRequired; + } + + public boolean isSetterRequired() { + return setterRequired; + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorValidator.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorValidator.java new file mode 100644 index 00000000000..3f5db988bcb --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorValidator.java @@ -0,0 +1,226 @@ +package ai.timefold.solver.core.impl.domain.common.accessor; + +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +final class MemberAccessorValidator { + private MemberAccessorValidator() { + } + + public static void verifyIsValidMember(@Nullable Class annotationClass, Member member, + MemberAccessorType memberAccessorType) { + var memberType = (member instanceof Field) ? "field" : "method"; + var messagePrefix = (annotationClass == null) + ? "The %s (%s) in class (%s)".formatted(memberType, member.getName(), + member.getDeclaringClass().getCanonicalName()) + : "The @%s annotated %s (%s) in class (%s)".formatted(annotationClass.getSimpleName(), memberType, + member.getName(), + member.getDeclaringClass().getCanonicalName()); + + verifyDeclaringClassIsAccessible(member, messagePrefix); + switch (memberAccessorType) { + case FIELD_OR_READ_METHOD -> verifyIsPublicFieldOrHasReadMethod(member, messagePrefix, false); + case FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER -> + verifyIsPublicFieldOrHasReadMethod(member, messagePrefix, true); + case FIELD_OR_GETTER_METHOD -> verifyFieldOrGetter(member, messagePrefix); + case VOID_METHOD -> verifyIsVoidMethod(member, messagePrefix); + case FIELD_OR_GETTER_METHOD_WITH_SETTER -> { + verifyFieldOrGetter(member, messagePrefix); + verifyIsPublicFieldOrHasPublicSetter(member, messagePrefix); + } + } + } + + private static void verifyDeclaringClassIsAccessible(Member member, String messagePrefix) { + var declaringClass = member.getDeclaringClass(); + if (!Modifier.isPublic(declaringClass.getModifiers())) { + throw new IllegalArgumentException( + "%s is not accessible because its declaring class (%s) is not public. Maybe make the class (%s) public?" + .formatted(messagePrefix, declaringClass.getCanonicalName(), declaringClass.getSimpleName())); + } + } + + private static void verifyIsVoidMethod(Member member, String messagePrefix) { + if (!(member instanceof Method method)) { + throw new IllegalArgumentException( + "%s must be a void method, but is a field instead.".formatted(messagePrefix)); + } + if (!method.getReturnType().equals(void.class)) { + throw new IllegalArgumentException("%s must be a void method, but it returns (%s).".formatted(messagePrefix, + method.getReturnType().getCanonicalName())); + } + if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException( + "%s is a void method, but it is not public. Maybe make the method (%s) public?" + .formatted(messagePrefix, method.getName())); + } + } + + private static void verifyGetterName(Method method, String messagePrefix) { + if (!method.getName().startsWith("get") && !(method.getReturnType().equals(boolean.class) && method.getName() + .startsWith("is"))) { + throw new IllegalArgumentException(""" + %s is suppose to be a public getter method, but its name (%s) does not start with "get"%s. + Maybe add a "get" prefix to the method? + """.formatted(messagePrefix, method.getName(), + method.getReturnType().equals(boolean.class) ? " or \"is\"" : "")); + } + } + + private static void verifyFieldOrGetter(Member member, String messagePrefix) { + if (member instanceof Field field) { + verifyIsPublicFieldOrHasPublicGetter(field, messagePrefix); + } else if (member instanceof Method method) { + verifyGetterName(method, messagePrefix); + if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException( + "%s is a getter method, but it is not public. Maybe make the method (%s) public?" + .formatted(messagePrefix, method.getName())); + } + if (method.getReturnType().equals(void.class)) { + throw new IllegalArgumentException( + "%s has a public getter method that returns void. Maybe make the method (%s) return a value instead?" + .formatted(messagePrefix, method.getName())); + } + } else { + throw new IllegalArgumentException("Unhandled member type (%s)." + .formatted(member.getClass().getCanonicalName())); + } + } + + private static void verifyIsPublicFieldOrHasPublicGetter(Field field, String messagePrefix) { + var getterMethod = ReflectionHelper.getGetterMethod(field.getDeclaringClass(), field.getName()); + if (getterMethod == null) { + if (!Modifier.isPublic(field.getModifiers())) { + throw new IllegalArgumentException( + "%s is not public and does not have a public getter method. Maybe add a public getter method?" + .formatted(messagePrefix)); + } + } else { + if (!Modifier.isPublic(getterMethod.getModifiers())) { + throw new IllegalArgumentException( + "%s has a non-public getter method. Maybe make the method (%s) public?" + .formatted(messagePrefix, getterMethod.getName())); + } + if (getterMethod.getReturnType().equals(void.class)) { + throw new IllegalArgumentException( + "%s has a public getter method that returns void. Maybe make the method (%s) return (%s) instead?" + .formatted(messagePrefix, getterMethod.getName(), field.getType().getCanonicalName())); + } + } + } + + private static void verifyIsPublicFieldOrHasReadMethod(Member member, String messagePrefix, + boolean acceptOptionalParameter) { + if (member instanceof Field field) { + verifyIsPublicFieldOrHasPublicGetter(field, messagePrefix); + } else if (member instanceof Method method) { + if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException( + "%s is a read method, but it is not public. Maybe make the method (%s) public?" + .formatted(messagePrefix, method.getName())); + } + if (method.getReturnType().equals(void.class)) { + throw new IllegalArgumentException( + "%s is a public read method, but it returns void. Maybe make the method (%s) return a value instead?" + .formatted(messagePrefix, method.getName())); + } + + if (acceptOptionalParameter) { + if (method.getParameterCount() > 1) { + throw new IllegalArgumentException(""" + %s is a public read method, but takes (%d) parameters instead of zero or one. + Maybe make the method (%s) take zero or one parameter(s)? + """.formatted(messagePrefix, method.getParameterCount(), method.getName())); + } + } else if (method.getParameterCount() != 0) { + throw new IllegalArgumentException(""" + %s is a public read method, but takes (%d) parameters instead of none. + Maybe make the method (%s) take no parameters? + """.formatted(messagePrefix, method.getParameterCount(), method.getName())); + } + } else { + throw new IllegalArgumentException("Unhandled member type (%s)." + .formatted(member.getClass().getCanonicalName())); + } + } + + private static void verifyIsPublicFieldOrHasPublicSetter(Member member, + String messagePrefix) { + if (member instanceof Field field) { + var getterMethod = ReflectionHelper.getGetterMethod(field.getDeclaringClass(), field.getName()); + var setterMethod = ReflectionHelper.getSetterMethod(field.getDeclaringClass(), field.getName()); + if (setterMethod == null) { + if (!Modifier.isPublic(field.getModifiers())) { + throw new IllegalArgumentException( + "%s does not have a setter method and is not public. Maybe add a public setter method?" + .formatted(messagePrefix)); + } + } else { + if (getterMethod == null) { + throw new IllegalArgumentException( + "%s has a setter method (%s) without a getter method. Maybe add a public getter method?" + .formatted(member, setterMethod.getName())); + } + verifyGetterSetterProperties(getterMethod, setterMethod, messagePrefix); + } + } else if (member instanceof Method getterMethod) { + verifyGetterName(getterMethod, messagePrefix); + var memberName = ReflectionHelper.getGetterPropertyName(getterMethod); + var setterMethod = ReflectionHelper.getSetterMethod(getterMethod.getDeclaringClass(), + memberName); + if (setterMethod == null) { + throw new IllegalArgumentException(""" + %s requires both a public getter and a public setter but only have a public getter. + Maybe add a public setter for the member (%s)? + """.formatted(messagePrefix, memberName)); + } + verifyGetterSetterProperties(getterMethod, setterMethod, messagePrefix); + } else { + throw new IllegalArgumentException("Unhandled member type (%s)." + .formatted(member.getClass().getCanonicalName())); + } + } + + private static void verifyGetterSetterProperties(Method getterMethod, Method setterMethod, String messagePrefix) { + if (!Modifier.isPublic(getterMethod.getModifiers())) { + throw new IllegalArgumentException( + "%s has a non-public getter method. Maybe make the method (%s) public?" + .formatted(messagePrefix, getterMethod.getName())); + } + if (!Modifier.isPublic(setterMethod.getModifiers())) { + throw new IllegalArgumentException( + "%s has a non-public setter method. Maybe make the method (%s) public?" + .formatted(messagePrefix, setterMethod.getName())); + } + if (setterMethod.getParameterCount() != 1) { + throw new IllegalArgumentException(""" + %s has a public setter method that takes (%d) parameters instead of one. + Maybe make the method (%s) take exactly one parameter?""" + .formatted(messagePrefix, setterMethod.getParameterCount(), + setterMethod.getName())); + } + if (!setterMethod.getParameterTypes()[0].isAssignableFrom(getterMethod.getReturnType())) { + throw new IllegalArgumentException( + """ + %s has a public setter method but its parameter type (%s) is not assignable from the getter return type (%s). + Maybe make the public setter (%s) accept (%s)?""" + .formatted(messagePrefix, setterMethod.getParameterTypes()[0].getCanonicalName(), + getterMethod.getReturnType().getCanonicalName(), setterMethod.getName(), + getterMethod.getReturnType().getCanonicalName())); + } + if (!setterMethod.getReturnType().equals(void.class)) { + throw new IllegalArgumentException( + "%s has a public getter method that returns void. Maybe make the method (%s) return (%s) instead?" + .formatted(messagePrefix, setterMethod.getName(), getterMethod.getReturnType().getCanonicalName())); + } + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java index 0d02de96a62..8ef1451d988 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java @@ -5,10 +5,8 @@ import java.lang.invoke.MethodHandles; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.Objects; -import java.util.function.IntPredicate; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; @@ -44,10 +42,6 @@ public ReflectionBeanPropertyMemberAccessor(Method getterMethod, AnnotatedElemen .formatted(getterMethod, MemberAccessorFactory.CLASSLOADER_NUDGE_MESSAGE), e); } Class declaringClass = getterMethod.getDeclaringClass(); - if (!ReflectionHelper.isGetterMethod(getterMethod)) { - throw new IllegalArgumentException("The getterMethod (%s) is not a valid getter." - .formatted(getterMethod)); - } propertyType = getterMethod.getReturnType(); propertyName = ReflectionHelper.getGetterPropertyName(getterMethod); if (getterOnly) { @@ -55,23 +49,6 @@ public ReflectionBeanPropertyMemberAccessor(Method getterMethod, AnnotatedElemen setterMethodHandle = null; } else { setterMethod = ReflectionHelper.getDeclaredSetterMethod(declaringClass, getterMethod.getReturnType(), propertyName); - if (setterMethod == null) { - throw new IllegalArgumentException("The getterMethod (%s) does not have a matching setterMethod on class (%s)." - .formatted(getterMethod.getName(), declaringClass.getCanonicalName())); - } - var getterAccess = AccessModifier.forMethod(getterMethod); - var setterAccess = AccessModifier.forMethod(setterMethod); - if (getterAccess != AccessModifier.PUBLIC) { - throw new IllegalArgumentException( - "The getterMethod (%s) on class (%s) is not public, having access modifier (%s) instead." - .formatted(getterMethod.getName(), declaringClass.getCanonicalName(), getterAccess)); - } - if (getterAccess != setterAccess) { - throw new IllegalArgumentException( - "The getterMethod (%s) has access modifier (%s) which does not match the setterMethod (%s) access modifier (%s) on class (%s)." - .formatted(getterMethod.getName(), getterAccess, setterMethod.getName(), setterAccess, - declaringClass.getCanonicalName())); - } try { this.setterMethod.setAccessible(true); this.setterMethodHandle = lookup.unreflect(setterMethod) @@ -85,36 +62,6 @@ public ReflectionBeanPropertyMemberAccessor(Method getterMethod, AnnotatedElemen } } - private enum AccessModifier { - PUBLIC("public", Modifier::isPublic), - PROTECTED("protected", Modifier::isProtected), - PACKAGE_PRIVATE("package-private", modifier -> false), - PRIVATE("private", Modifier::isPrivate); - - final String name; - final IntPredicate predicate; - - AccessModifier(String name, IntPredicate predicate) { - this.name = name; - this.predicate = predicate; - } - - public static AccessModifier forMethod(Method method) { - var modifiers = method.getModifiers(); - for (var accessModifier : AccessModifier.values()) { - if (accessModifier.predicate.test(modifiers)) { - return accessModifier; - } - } - return PACKAGE_PRIVATE; - } - - @Override - public String toString() { - return name; - } - } - @Override public Class getDeclaringClass() { return getterMethod.getDeclaringClass(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodExtendedMemberAccessor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodExtendedMemberAccessor.java index 76d7b46af3a..9f4e8ac0862 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodExtendedMemberAccessor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodExtendedMemberAccessor.java @@ -9,11 +9,7 @@ public final class ReflectionMethodExtendedMemberAccessor extends ReflectionMeth private final Type getterMethodParameterType; public ReflectionMethodExtendedMemberAccessor(Method readMethod) { - this(readMethod, true); - } - - public ReflectionMethodExtendedMemberAccessor(Method readMethod, boolean returnTypeRequired) { - super(readMethod, returnTypeRequired, true); + super(readMethod); this.getterMethodParameterType = readMethod.getGenericParameterTypes()[0]; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodMemberAccessor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodMemberAccessor.java index f35abe5d38c..5117f0b6870 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodMemberAccessor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodMemberAccessor.java @@ -5,7 +5,6 @@ import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.lang.reflect.Type; -import java.util.Arrays; /** * A {@link MemberAccessor} based on a single read {@link Method}. @@ -20,10 +19,6 @@ public sealed class ReflectionMethodMemberAccessor extends AbstractMemberAccesso private final MethodHandle methodHandle; public ReflectionMethodMemberAccessor(Method readMethod) { - this(readMethod, true, false); - } - - public ReflectionMethodMemberAccessor(Method readMethod, boolean returnTypeRequired, boolean readMethodWithParameter) { this.readMethod = readMethod; this.returnType = readMethod.getReturnType(); this.methodName = readMethod.getName(); @@ -37,18 +32,6 @@ public ReflectionMethodMemberAccessor(Method readMethod, boolean returnTypeRequi %s """.formatted(readMethod, MemberAccessorFactory.CLASSLOADER_NUDGE_MESSAGE), e); } - if (!readMethodWithParameter && readMethod.getParameterCount() != 0) { - throw new IllegalArgumentException("The readMethod (%s) must not have any parameters (%s).".formatted(readMethod, - Arrays.toString(readMethod.getParameterTypes()))); - } - if (readMethodWithParameter && readMethod.getParameterCount() > 1) { - throw new IllegalArgumentException("The readMethod (%s) must have only one parameter (%s).".formatted(readMethod, - Arrays.toString(readMethod.getParameterTypes()))); - } - if (returnTypeRequired && readMethod.getReturnType() == void.class) { - throw new IllegalArgumentException( - "The readMethod (%s) must have a return type (%s).".formatted(readMethod, readMethod.getReturnType())); - } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/AccessorInfo.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/AccessorInfo.java index 575747ef0bd..347a7d46003 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/AccessorInfo.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/AccessorInfo.java @@ -1,6 +1,6 @@ package ai.timefold.solver.core.impl.domain.common.accessor.gizmo; -import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType; /** * Additional information for the GIZMO accessor generation. @@ -8,20 +8,20 @@ * @param returnTypeRequired a flag that indicates if the return type is required or optional * @param readMethodWithParameter a flag that allows the read method to accept an argument */ -public record AccessorInfo(MemberAccessorFactory.MemberAccessorType memberAccessorType, boolean returnTypeRequired, +public record AccessorInfo(MemberAccessorType memberAccessorType, boolean returnTypeRequired, boolean readMethodWithParameter) { public static AccessorInfo withReturnValueAndNoArguments() { - return new AccessorInfo(MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD, true, false); + return new AccessorInfo(MemberAccessorType.FIELD_OR_READ_METHOD, true, false); } public static AccessorInfo withReturnValueAndArguments() { - return new AccessorInfo(MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, true, + return new AccessorInfo(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, true, true); } - public static AccessorInfo of(MemberAccessorFactory.MemberAccessorType memberAccessorType) { - return new AccessorInfo(memberAccessorType, memberAccessorType != MemberAccessorFactory.MemberAccessorType.VOID_METHOD, - memberAccessorType == MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER); + public static AccessorInfo of(MemberAccessorType memberAccessorType) { + return new AccessorInfo(memberAccessorType, memberAccessorType != MemberAccessorType.VOID_METHOD, + memberAccessorType == MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java index a648c677423..2845a2697dc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java @@ -1,16 +1,27 @@ package ai.timefold.solver.core.impl.domain.common.accessor.gizmo; +import java.lang.constant.ClassDesc; +import java.lang.invoke.MethodHandles; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import io.quarkus.gizmo2.Gizmo; +import io.quarkus.gizmo2.desc.ConstructorDesc; /** * Loads a class if we have the Gizmo-generated bytecode for it, * otherwise uses the current {@link Thread}'s context {@link ClassLoader}. * This implementation is thread-safe. */ +@NullMarked public final class GizmoClassLoader extends ClassLoader { private final Map classNameToBytecodeMap; + @Nullable private GizmoSupportStatus gizmoSupportStatus; public GizmoClassLoader() { @@ -25,15 +36,7 @@ public GizmoClassLoader(Map classNameToBytecodeMap) { */ super(GizmoClassLoader.class.getClassLoader()); this.classNameToBytecodeMap = classNameToBytecodeMap; - this.gizmoSupportStatus = GizmoSupportStatus.UNKNOWN; - } - - public GizmoSupportStatus getGizmoSupportStatus() { - return gizmoSupportStatus; - } - - public void setGizmoSupportStatus(GizmoSupportStatus gizmoSupportStatus) { - this.gizmoSupportStatus = gizmoSupportStatus; + this.gizmoSupportStatus = null; } @Override @@ -63,4 +66,43 @@ public synchronized void storeBytecode(String className, byte[] bytecode) { classNameToBytecodeMap.put(className, bytecode); } + public boolean isGizmoSupported() { + if (gizmoSupportStatus != null) { + return gizmoSupportStatus == GizmoSupportStatus.SUPPORTED; + } + var classPackage = GizmoClassLoader.class.getPackage().getName(); + var bytecodeHolder = new AtomicReference(); + var gizmo = Gizmo.create((className, bytecode) -> bytecodeHolder.set(bytecode)); + + var classDesc = ClassDesc.of("%s.GizmoSupportCanary".formatted(classPackage)); + gizmo.class_(classDesc, + classCreator -> classCreator.constructor(ConstructorDesc.of(classDesc), constructorCreator -> { + constructorCreator.public_(); + var this_ = constructorCreator.this_(); + constructorCreator.body(constructor -> { + constructor.invokeSpecial(ConstructorDesc.of(Object.class), this_); + constructor.return_(); + }); + })); + try { + var generatedClass = MethodHandles.lookup().defineHiddenClass(bytecodeHolder.get(), true).lookupClass(); + var instance = generatedClass.getConstructor().newInstance(); + if (instance == null) { + // Should be impossible, but a native image might decide to optimize out + // instance if it is unused + gizmoSupportStatus = GizmoSupportStatus.UNSUPPORTED; + return false; + } else { + gizmoSupportStatus = GizmoSupportStatus.SUPPORTED; + return true; + } + } catch (Throwable e) { + // Note: GraalVM will throw a com.oracle.svm.core.jdk.UnsupportedFeatureError + // on defineHiddenClass, so we catch "Throwable" here so we don't need + // to add GraalVM as a library + gizmoSupportStatus = GizmoSupportStatus.UNSUPPORTED; + return false; + } + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoFieldHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoFieldHandler.java index da23970f7d5..6684292eeff 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoFieldHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoFieldHandler.java @@ -23,34 +23,17 @@ final class GizmoFieldHandler implements GizmoMemberHandler { private final @Nullable MethodDesc setterDescriptor; private final boolean canBeWritten; - GizmoFieldHandler(Class declaringClass, FieldDesc fieldDescriptor, boolean ignoreChecks, boolean canBeWritten, - boolean isFieldPublic) { + GizmoFieldHandler(Class declaringClass, FieldDesc fieldDescriptor, boolean ignoreChecks, boolean canBeWritten) { this.declaringClass = declaringClass; this.fieldDescriptor = fieldDescriptor; var getterMethod = ReflectionHelper.getGetterMethod(declaringClass, fieldDescriptor.name()); var setterMethod = ReflectionHelper.getSetterMethod(declaringClass, fieldDescriptor.name()); if (getterMethod == null) { - if (setterMethod != null) { - throw new IllegalArgumentException("Field (%s) in class (%s) is a write-only field." - .formatted(fieldDescriptor.name(), declaringClass.getName())); - } - if (!ignoreChecks && !isFieldPublic) { - throw new IllegalArgumentException( - """ - Member (%s) of class (%s) is not public.""" - .formatted(fieldDescriptor.name(), declaringClass.getName())); - } getterDescriptor = null; setterDescriptor = null; this.canBeWritten = canBeWritten; } else { - if (!ignoreChecks && !Modifier.isPublic(getterMethod.getModifiers())) { - throw new IllegalArgumentException( - """ - Member (%s) of class (%s) is not public.""" - .formatted(getterMethod.getName(), getterMethod.getDeclaringClass().getName())); - } ReflectionHelper.assertGetterMethod(getterMethod); getterDescriptor = MethodDesc.of(getterMethod); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorFactory.java index 57ef0b793d1..49c7e69d6bc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorFactory.java @@ -1,20 +1,13 @@ package ai.timefold.solver.core.impl.domain.common.accessor.gizmo; import java.lang.annotation.Annotation; -import java.lang.constant.ClassDesc; -import java.lang.invoke.MethodHandles; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; -import io.quarkus.gizmo2.Gizmo; -import io.quarkus.gizmo2.desc.ConstructorDesc; - public class GizmoMemberAccessorFactory { /** * Returns the generated class name for a given member. @@ -44,53 +37,6 @@ public static MemberAccessor buildGizmoMemberAccessor(Member member, Class true; - case UNSUPPORTED -> false; - case UNKNOWN -> { - var classPackage = GizmoMemberAccessorFactory.class.getPackage().getName(); - var bytecodeHolder = new AtomicReference(); - var gizmo = Gizmo.create((className, bytecode) -> { - bytecodeHolder.set(bytecode); - }); - - var classDesc = ClassDesc.of("%s.Test".formatted(classPackage)); - gizmo.class_(classDesc, classCreator -> { - classCreator.constructor(ConstructorDesc.of(classDesc), constructorCreator -> { - constructorCreator.public_(); - var this_ = constructorCreator.this_(); - constructorCreator.body(constructor -> { - constructor.invokeSpecial(ConstructorDesc.of(Object.class), this_); - constructor.return_(); - }); - }); - }); - try { - var generatedClass = MethodHandles.lookup().defineHiddenClass(bytecodeHolder.get(), true).lookupClass(); - var instance = generatedClass.getConstructor().newInstance(); - if (instance == null) { - // Should be impossible, but a native image might decide to optimize out - // instance if it is unused - gizmoClassLoader.setGizmoSupportStatus(GizmoSupportStatus.UNSUPPORTED); - yield false; - } else { - gizmoClassLoader.setGizmoSupportStatus(GizmoSupportStatus.SUPPORTED); - yield true; - } - } catch (IllegalAccessException | NoSuchMethodException | InstantiationException - | InvocationTargetException | Error e) { - // Note: GraalVM will throw a com.oracle.svm.core.jdk.UnsupportedFeatureError - // on defineHiddenClass, so we also catch "Error" here so we don't need - // to add GraalVM as a library - gizmoClassLoader.setGizmoSupportStatus(GizmoSupportStatus.UNSUPPORTED); - yield false; - } - } - }; - - } - private GizmoMemberAccessorFactory() { } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementor.java index ddb3125df1a..42f65821d41 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementor.java @@ -2,22 +2,17 @@ import java.lang.annotation.Annotation; import java.lang.constant.ClassDesc; -import java.lang.constant.ConstantDescs; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Type; -import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; -import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; import ai.timefold.solver.core.impl.util.MutableReference; -import org.jspecify.annotations.NonNull; - import io.quarkus.gizmo2.ClassOutput; import io.quarkus.gizmo2.Const; import io.quarkus.gizmo2.Gizmo; @@ -95,8 +90,8 @@ public static void defineAccessorFor(String className, ClassOutput classOutput, } private static Class getCorrectSuperclass(GizmoMemberInfo memberInfo) { - AtomicBoolean supportsSetter = new AtomicBoolean(); - AtomicBoolean methodWithParameter = new AtomicBoolean(); + var supportsSetter = new AtomicBoolean(); + var methodWithParameter = new AtomicBoolean(); memberInfo.descriptor().whenIsMethod(method -> { supportsSetter.set(memberInfo.descriptor().getSetter().isPresent()); methodWithParameter.set(memberInfo.readMethodWithParameter()); @@ -130,17 +125,17 @@ private static Class getCorrectSuperclass */ static MemberAccessor createAccessorFor(Member member, Class annotationClass, AccessorInfo accessorInfo, GizmoClassLoader gizmoClassLoader) { - String className = GizmoMemberAccessorFactory.getGeneratedClassName(member); + var className = GizmoMemberAccessorFactory.getGeneratedClassName(member); if (gizmoClassLoader.hasBytecodeFor(className)) { return createInstance(className, gizmoClassLoader); } - final MutableReference classBytecodeHolder = new MutableReference<>(null); + var classBytecodeHolder = new MutableReference(null); ClassOutput classOutput = (path, byteCode) -> classBytecodeHolder.setValue(byteCode); var descriptor = new GizmoMemberDescriptor(member, accessorInfo); - GizmoMemberInfo memberInfo = new GizmoMemberInfo(descriptor, accessorInfo.returnTypeRequired(), + var memberInfo = new GizmoMemberInfo(descriptor, accessorInfo.returnTypeRequired(), descriptor.getMethodParameterType() != null, annotationClass); defineAccessorFor(className, classOutput, memberInfo); - byte[] classBytecode = classBytecodeHolder.getValue(); + var classBytecode = classBytecodeHolder.getValue(); gizmoClassLoader.storeBytecode(className, classBytecode); return createInstance(className, gizmoClassLoader); @@ -232,103 +227,6 @@ private static void createGetDeclaringClass(GeneratedClassInfo generatedClassInf }); } - /** - * Asserts method is a getter or read method - * - * @param method Method to assert is getter or read - * @param accessorInfo What kind of {@link MemberAccessor} is being generated - */ - private static void assertIsGoodMethod(MethodDesc method, AccessorInfo accessorInfo) { - // V = void return type - // Z = primitive boolean return type - String methodName = method.name(); - if (!accessorInfo.readMethodWithParameter() && method.parameterCount() != 0) { - // not read or getter method - throw new IllegalStateException("The getterMethod (%s) must not have any parameters, but has parameters (%s)." - .formatted(methodName, Arrays.toString(method.parameterTypes().toArray()))); - } - - var memberAccessorType = accessorInfo.memberAccessorType(); - - var methodType = (methodName.startsWith("get") || methodName.startsWith("is")) ? "getterMethod" : "readMethod"; - if (memberAccessorType == MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD || - memberAccessorType == MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER) { - // Only enforce getters/setters naming rules for getters/setters - // we don't want to enforce them on @ShadowSources suppliers - if (methodName.startsWith("get")) { - if (method.returnType().equals(ConstantDescs.CD_void)) { - throw new IllegalStateException("The getterMethod (%s) must have a non-void return type." - .formatted(methodName)); - } - } else if (methodName.startsWith("is")) { - if (!method.returnType().equals(ConstantDescs.CD_boolean)) { - throw new IllegalStateException(""" - The getterMethod (%s) must have a primitive boolean return type but returns (%s). - Maybe rename the method (get%s)?""" - .formatted(methodName, method.returnType(), methodName.substring(2))); - } - } - } - if (memberAccessorType != MemberAccessorFactory.MemberAccessorType.VOID_METHOD) { - // must have a return type - if (method.returnType().equals(ConstantDescs.CD_void)) { - throw new IllegalStateException("The %s (%s) must have a non-void return type." - .formatted(methodType, methodName)); - } - } - } - - /** - * Asserts method is a getter or read method - * - * @param method Method to assert is getter or read method - * @param accessorInfo What kind of {@link MemberAccessor} is being generated - */ - private static void assertIsGoodMethod(MethodDesc method, AccessorInfo accessorInfo, - Class annotationClass) { - // V = void return type - // Z = primitive boolean return type - String methodName = method.name(); - if (!accessorInfo.readMethodWithParameter() && method.parameterCount() != 0) { - // not read or getter method - throw new IllegalStateException( - "The getterMethod (%s) with a %s annotation must not have any parameters, but has parameters (%s)." - .formatted(methodName, annotationClass.getSimpleName(), - method.parameterTypes().stream().map(ClassDesc::descriptorString).toList())); - } - - var memberAccessorType = accessorInfo.memberAccessorType(); - var methodType = (methodName.startsWith("get") || methodName.startsWith("is")) ? "getterMethod" : "readMethod"; - - if (memberAccessorType == MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD || - memberAccessorType == MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER) { - if (methodName.startsWith("get")) { - if (method.returnType().equals(ConstantDescs.CD_void)) { - throw new IllegalStateException( - "The getterMethod (%s) with a @%s annotation must have a non-void return type." - .formatted(methodName, annotationClass.getSimpleName())); - } - } else if (methodName.startsWith("is")) { - if (!method.returnType().equals(ConstantDescs.CD_boolean)) { - throw new IllegalStateException( - """ - The %s (%s) with a @%s annotation must have a primitive boolean return type but returns (%s). - Maybe rename the method (get%s)?""" - .formatted(methodType, methodName, annotationClass.getSimpleName(), method.returnType() - .descriptorString(), - methodName.substring(2))); - } - } - } - if (memberAccessorType != MemberAccessorFactory.MemberAccessorType.VOID_METHOD) { - // must be a read method - if (accessorInfo.returnTypeRequired() && method.returnType().equals(ConstantDescs.CD_void)) { - throw new IllegalStateException("The %s (%s) with a @%s annotation must have a non-void return type." - .formatted(methodType, methodName, annotationClass.getSimpleName())); - } - } - } - /** * Generates the following code: * @@ -349,18 +247,6 @@ private static void createGetName(GeneratedClassInfo generatedClassInfo) { builder.public_(); builder.returning(String.class); builder.body(blockCreator -> { - // 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, memberInfo.descriptor().getAccessorInfo()); - } else { - assertIsGoodMethod(method, memberInfo.descriptor().getAccessorInfo(), - annotationClass); - } - }); - String fieldName = memberInfo.descriptor().getName(); blockCreator.return_(Const.of(fieldName)); }); @@ -525,8 +411,6 @@ private static void createExecuteGetterWithParameter(GeneratedClassInfo generate builder.returning(Object.class); var bean = builder.parameter("bean", Object.class); var value = builder.parameter("value", Object.class); - memberInfo.descriptor().whenIsMethod( - md -> assertIsGoodMethod(md, memberInfo.descriptor().getAccessorInfo())); builder.body(blockCreator -> { var castedBean = blockCreator.localVar("castedBean", ClassDesc.of(memberInfo.descriptor().getDeclaringClassName()), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberDescriptor.java index 05d628d9047..2d4fc449611 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberDescriptor.java @@ -10,7 +10,7 @@ import java.util.function.Consumer; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; -import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -126,7 +126,7 @@ public GizmoMemberDescriptor(String name, MethodDesc memberDescriptor, FieldDesc this.metadataHandler = GizmoMemberHandler.of(declaringClass, name, metadataDescriptor, true); this.methodParameterType = methodParameterType; this.setter = setterDescriptor; - this.accessorInfo = AccessorInfo.of(MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD); + this.accessorInfo = AccessorInfo.of(MemberAccessorType.FIELD_OR_READ_METHOD); } @Nullable diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberHandler.java index d83ff9ca958..197c40c67ed 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberHandler.java @@ -28,10 +28,9 @@ static GizmoMemberHandler of(Class declaringClass, String name, FieldDesc fie try { Field field = declaringClass.getField(name); return new GizmoFieldHandler(declaringClass, fieldDescriptor, - ignoreFinalChecks, ignoreFinalChecks || !Modifier.isFinal(field.getModifiers()), - Modifier.isPublic(field.getModifiers())); + ignoreFinalChecks, ignoreFinalChecks || !Modifier.isFinal(field.getModifiers())); } catch (NoSuchFieldException e) { // The field is only used for its metadata and never actually called. - return new GizmoFieldHandler(declaringClass, fieldDescriptor, ignoreFinalChecks, false, false); + return new GizmoFieldHandler(declaringClass, fieldDescriptor, ignoreFinalChecks, false); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoSupportStatus.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoSupportStatus.java index f27f8a830fd..33fd980fde2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoSupportStatus.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoSupportStatus.java @@ -2,6 +2,5 @@ public enum GizmoSupportStatus { SUPPORTED, - UNSUPPORTED, - UNKNOWN; + UNSUPPORTED } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java index 46495afc4b5..8ced593a76e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java @@ -1,8 +1,8 @@ package ai.timefold.solver.core.impl.domain.entity.descriptor; -import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER; -import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD; -import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER; +import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER; +import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType.FIELD_OR_READ_METHOD; +import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER; import static ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptorValidator.assertNotMixedInheritance; import static ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptorValidator.assertSingleInheritance; import static ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptorValidator.assertValidPlanningVariables; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/policy/DescriptorPolicy.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/policy/DescriptorPolicy.java index 3e843b2eaac..57775043947 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/policy/DescriptorPolicy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/policy/DescriptorPolicy.java @@ -1,6 +1,6 @@ package ai.timefold.solver.core.impl.domain.policy; -import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER; +import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER; import java.lang.reflect.Member; import java.util.ArrayList; @@ -58,7 +58,7 @@ public class DescriptorPolicy { private final Set anonymousFromSolutionValueRangeProviderSet = new LinkedHashSet<>(); private final Map fromEntityValueRangeProviderMap = new LinkedHashMap<>(); private final Set anonymousFromEntityValueRangeProviderSet = new LinkedHashSet<>(); - private DomainAccessType domainAccessType = DomainAccessType.REFLECTION; + private DomainAccessType domainAccessType = DomainAccessType.FORCE_REFLECTION; private Set enabledPreviewFeatureSet = EnumSet.noneOf(PreviewFeature.class); @Nullable private MemberAccessorFactory memberAccessorFactory; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/OverridesBasedConstraintWeightSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/OverridesBasedConstraintWeightSupplier.java index 65ca1be1437..f42907e6b06 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/OverridesBasedConstraintWeightSupplier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/OverridesBasedConstraintWeightSupplier.java @@ -14,6 +14,7 @@ import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType; import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; @@ -37,7 +38,7 @@ public static > ConstraintWeightSupplier overridesClass = (Class>) method.getReturnType(); } var memberAccessor = descriptorPolicy.getMemberAccessorFactory().buildAndCacheMemberAccessor(member, - MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, descriptorPolicy.getDomainAccessType()); return new OverridesBasedConstraintWeightSupplier<>(solutionDescriptor, memberAccessor, overridesClass); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java index 73355fdaedc..9ff642c5adf 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java @@ -1,7 +1,7 @@ package ai.timefold.solver.core.impl.domain.solution.descriptor; -import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD; -import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD; +import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType.FIELD_OR_GETTER_METHOD; +import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType.FIELD_OR_READ_METHOD; import static ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor.extractInheritedClasses; import static java.util.stream.Stream.concat; @@ -116,7 +116,7 @@ public static SolutionDescriptor buildSolutionDescriptor( Set enabledPreviewFeaturesSet, Class solutionClass, List> entityClassList) { - return buildSolutionDescriptor(enabledPreviewFeaturesSet, DomainAccessType.REFLECTION, solutionClass, null, + return buildSolutionDescriptor(enabledPreviewFeaturesSet, DomainAccessType.FORCE_REFLECTION, solutionClass, null, null, entityClassList); } @@ -724,9 +724,9 @@ private void initSolutionCloner(DescriptorPolicy descriptorPolicy) { } if (solutionCloner == null) { solutionCloner = switch (descriptorPolicy.getDomainAccessType()) { - case GIZMO -> GizmoSolutionClonerFactory.build(this, memberAccessorFactory.getGizmoClassLoader()); + case FORCE_GIZMO -> GizmoSolutionClonerFactory.build(this, memberAccessorFactory.getGizmoClassLoader()); // AUTO means we are probably in plain Java, so we need to use reflection so we can clone final fields - case AUTO, REFLECTION -> new FieldAccessingSolutionCloner<>(this); + case AUTO, FORCE_REFLECTION -> new FieldAccessingSolutionCloner<>(this); }; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/cascade/CascadingUpdateShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/cascade/CascadingUpdateShadowVariableDescriptor.java index 77bfa0f552b..1c2f9be3924 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/cascade/CascadingUpdateShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/cascade/CascadingUpdateShadowVariableDescriptor.java @@ -14,7 +14,7 @@ import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; -import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; import ai.timefold.solver.core.impl.domain.variable.descriptor.ShadowVariableDescriptor; @@ -130,7 +130,7 @@ public void linkVariableDescriptors(DescriptorPolicy descriptorPolicy) { targetMethodName)); } targetMethod = descriptorPolicy.getMemberAccessorFactory().buildAndCacheMemberAccessor(allSourceMethodMembers.get(0), - MemberAccessorFactory.MemberAccessorType.VOID_METHOD, null, descriptorPolicy.getDomainAccessType()); + MemberAccessorType.VOID_METHOD, null, descriptorPolicy.getDomainAccessType()); firstTargetVariableDescriptor = targetVariableDescriptorList.get(0); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DeclarativeShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DeclarativeShadowVariableDescriptor.java index c7e35bff5a3..fca3814d874 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DeclarativeShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DeclarativeShadowVariableDescriptor.java @@ -11,7 +11,7 @@ import ai.timefold.solver.core.api.domain.variable.ShadowVariable; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; -import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; import ai.timefold.solver.core.impl.domain.variable.descriptor.ShadowVariableDescriptor; @@ -71,7 +71,7 @@ Maybe you included a parameter which is not a planning solution (%s)? } this.calculator = entityDescriptor.getSolutionDescriptor().getMemberAccessorFactory().buildAndCacheMemberAccessor(method, - MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, ShadowSources.class, descriptorPolicy.getDomainAccessType()); @@ -134,7 +134,7 @@ public void linkVariableDescriptors(DescriptorPolicy descriptorPolicy) { alignmentKey); if (alignmentKeyMember != null) { alignmentKeyMap = memberAccessorFactory.buildAndCacheMemberAccessor(alignmentKeyMember, - MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD, ShadowSources.class, + MemberAccessorType.FIELD_OR_GETTER_METHOD, ShadowSources.class, descriptorPolicy.getDomainAccessType())::executeGetter; } else { alignmentKeyMap = null; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSource.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSource.java index 624c20fb9ee..de226d1cdb7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSource.java @@ -19,6 +19,7 @@ import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType; import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel; import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel; @@ -363,7 +364,7 @@ public static Member getMember(Class rootClass, String sourcePath, Class d private static MemberAccessor getMemberAccessor(Member member, MemberAccessorFactory memberAccessorFactory, DescriptorPolicy descriptorPolicy) { return memberAccessorFactory.buildAndCacheMemberAccessor(member, - MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD, + MemberAccessorType.FIELD_OR_GETTER_METHOD, descriptorPolicy.getDomainAccessType()); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactoryTest.java index 3cfbc17386e..bba41f10ae5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorFactoryTest.java @@ -26,8 +26,8 @@ class MemberAccessorFactoryTest { void fieldAnnotatedEntityWithPublicGetter() throws NoSuchFieldException { MemberAccessor memberAccessor = MemberAccessorFactory.buildMemberAccessor(TestdataFieldAnnotatedEntity.class.getDeclaredField("value"), - MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, PlanningVariable.class, - DomainAccessType.REFLECTION, null); + MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, PlanningVariable.class, + DomainAccessType.FORCE_REFLECTION, null); assertThat(memberAccessor) .isInstanceOf(ReflectionBeanPropertyMemberAccessor.class); assertThat(memberAccessor.getName()).isEqualTo("value"); @@ -45,8 +45,8 @@ void fieldAnnotatedEntityWithPublicGetter() throws NoSuchFieldException { void privateField() { assertThatCode(() -> MemberAccessorFactory.buildMemberAccessor( TestdataVisibilityModifierSolution.class.getDeclaredField("privateField"), - MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, ProblemFactProperty.class, - DomainAccessType.REFLECTION, null)) + MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, ProblemFactProperty.class, + DomainAccessType.FORCE_REFLECTION, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContainingAll("The @ProblemFactProperty annotated field", "privateField", @@ -58,8 +58,8 @@ void privateField() { void publicField() throws NoSuchFieldException { MemberAccessor memberAccessor = MemberAccessorFactory.buildMemberAccessor( TestdataVisibilityModifierSolution.class.getDeclaredField("publicField"), - MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, ProblemFactProperty.class, - DomainAccessType.REFLECTION, null); + MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, ProblemFactProperty.class, + DomainAccessType.FORCE_REFLECTION, null); assertThat(memberAccessor) .isInstanceOf(ReflectionFieldMemberAccessor.class); assertThat(memberAccessor.getName()).isEqualTo("publicField"); @@ -77,8 +77,8 @@ void publicField() throws NoSuchFieldException { void publicProperty() throws NoSuchMethodException { MemberAccessor memberAccessor = MemberAccessorFactory.buildMemberAccessor( TestdataVisibilityModifierSolution.class.getDeclaredMethod("getPublicProperty"), - MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, ProblemFactProperty.class, - DomainAccessType.REFLECTION, null); + MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, ProblemFactProperty.class, + DomainAccessType.FORCE_REFLECTION, null); assertThat(memberAccessor) .isInstanceOf(ReflectionBeanPropertyMemberAccessor.class); assertThat(memberAccessor.getName()).isEqualTo("publicProperty"); @@ -95,7 +95,7 @@ void publicProperty() throws NoSuchMethodException { @Test void methodReturnVoid() throws NoSuchMethodException { MemberAccessor memberAccessor = MemberAccessorFactory.buildMemberAccessor(TestdataEntity.class.getMethod("updateValue"), - MemberAccessorFactory.MemberAccessorType.VOID_METHOD, null, DomainAccessType.REFLECTION, null); + MemberAccessorType.VOID_METHOD, null, DomainAccessType.FORCE_REFLECTION, null); assertThat(memberAccessor).isInstanceOf(ReflectionMethodMemberAccessor.class); assertThat(memberAccessor.getName()).isEqualTo("updateValue"); assertThat(memberAccessor.getType()).isEqualTo(void.class); @@ -119,8 +119,8 @@ void shouldUseGeneratedMemberAccessorIfExists() throws NoSuchMethodException { preexistingMemberAccessors.put(GizmoMemberAccessorFactory.getGeneratedClassName(member), mockMemberAccessor); MemberAccessorFactory memberAccessorFactory = new MemberAccessorFactory(preexistingMemberAccessors); MemberAccessor memberAccessor = memberAccessorFactory.buildAndCacheMemberAccessor(member, - MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, ProblemFactProperty.class, - DomainAccessType.REFLECTION); + MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, ProblemFactProperty.class, + DomainAccessType.FORCE_REFLECTION); assertThat(memberAccessor) .isSameAs(mockMemberAccessor); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorValidatorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorValidatorTest.java new file mode 100644 index 00000000000..669e6e3bd3f --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorValidatorTest.java @@ -0,0 +1,313 @@ +package ai.timefold.solver.core.impl.domain.common.accessor; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; + +import ai.timefold.solver.core.api.domain.common.PlanningId; + +import org.junit.jupiter.api.Test; + +class MemberAccessorValidatorTest { + @Test + void includeAnnotationInfoIfPresent() { + assertThatCode(() -> { + MemberAccessorValidator.verifyIsValidMember(PlanningId.class, + PublicClass.class.getDeclaredField("privateFieldWithoutGetter"), + MemberAccessorType.FIELD_OR_GETTER_METHOD); + }).isInstanceOf(IllegalArgumentException.class).hasMessageContainingAll( + "@%s annotated field".formatted(PlanningId.class.getSimpleName()), + "privateFieldWithoutGetter"); + + assertThatCode(() -> { + MemberAccessorValidator.verifyIsValidMember(PlanningId.class, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithPrivateGetter"), + MemberAccessorType.FIELD_OR_GETTER_METHOD); + }).isInstanceOf(IllegalArgumentException.class).hasMessageContainingAll( + "@%s annotated method".formatted(PlanningId.class.getSimpleName()), + "getPrivateFieldWithPrivateGetter"); + } + + @Test + void membersInPrivateClassFail() throws NoSuchFieldException, NoSuchMethodException { + assertFieldFails(MemberAccessorType.FIELD_OR_GETTER_METHOD, PrivateClass.class.getDeclaredField("publicField"), + "its declaring class (%s) is not public".formatted(PrivateClass.class.getCanonicalName())); + assertFieldFails(MemberAccessorType.FIELD_OR_GETTER_METHOD, + PrivateClass.class.getDeclaredField("privateFieldWithGetter"), + "its declaring class (%s) is not public".formatted(PrivateClass.class.getCanonicalName())); + assertMethodFails(MemberAccessorType.VOID_METHOD, PrivateClass.class.getDeclaredMethod("publicVoidMethod"), + "its declaring class (%s) is not public".formatted(PrivateClass.class.getCanonicalName())); + } + + @Test + void fieldOrGetterMethod() throws NoSuchFieldException, NoSuchMethodException { + assertFieldPasses(MemberAccessorType.FIELD_OR_GETTER_METHOD, PublicClass.class.getDeclaredField("publicField")); + assertFieldPasses(MemberAccessorType.FIELD_OR_GETTER_METHOD, + PublicClass.class.getDeclaredField("privateFieldWithGetter")); + assertFieldPasses(MemberAccessorType.FIELD_OR_GETTER_METHOD, + PublicClass.class.getDeclaredField("privateFieldWithGetterAndSetter")); + assertFieldPasses(MemberAccessorType.FIELD_OR_GETTER_METHOD, + PublicClass.class.getDeclaredField("privateFieldWithPrivateSetter")); + assertMethodPasses(MemberAccessorType.FIELD_OR_GETTER_METHOD, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithGetter")); + assertMethodPasses(MemberAccessorType.FIELD_OR_GETTER_METHOD, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithPrivateSetter")); + assertMethodPasses(MemberAccessorType.FIELD_OR_GETTER_METHOD, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithGetterAndSetter")); + + assertFieldFails(MemberAccessorType.FIELD_OR_GETTER_METHOD, + PublicClass.class.getDeclaredField("privateFieldWithoutGetter"), + "is not public and does not have a public getter method"); + assertFieldFails(MemberAccessorType.FIELD_OR_GETTER_METHOD, + PublicClass.class.getDeclaredField("privateFieldWithPrivateGetter"), + "does not have a public getter method"); + assertMethodFails(MemberAccessorType.FIELD_OR_GETTER_METHOD, PublicClass.class.getDeclaredMethod("getVoidMethod"), + "has a public getter method that returns void"); + assertMethodFails(MemberAccessorType.FIELD_OR_GETTER_METHOD, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithPrivateGetter"), + "is a getter method, but it is not public"); + assertMethodFails(MemberAccessorType.FIELD_OR_GETTER_METHOD, + PublicClass.class.getDeclaredMethod("publicReadMethod"), + "is suppose to be a public getter method, but its name (publicReadMethod) does not start with \"get\""); + } + + @Test + void fieldOrGetterMethodWithSetter() throws NoSuchFieldException, NoSuchMethodException { + assertFieldPasses(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredField("publicField")); + assertFieldPasses(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredField("privateFieldWithGetterAndSetter")); + assertFieldPasses(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredField("privateFieldWithGetterAndSetter")); + assertMethodPasses(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithGetterAndSetter")); + + assertFieldFails(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredField("privateFieldWithGetter"), + "does not have a setter method and is not public"); + assertFieldFails(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredField("privateFieldWithoutGetter"), + "is not public and does not have a public getter method"); + assertFieldFails(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredField("privateFieldWithPrivateGetter"), + "does not have a public getter method"); + assertFieldFails(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredField("privateFieldWithPrivateSetter"), + "does not have a setter method and is not public"); + + assertMethodFails(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithPrivateSetter"), + "requires both a public getter and a public setter but only have a public getter"); + assertMethodFails(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithGetter"), + "requires both a public getter and a public setter but only have a public getter"); + assertMethodFails(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredMethod("getVoidMethod"), + "has a public getter method that returns void"); + assertMethodFails(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredMethod("getVoidMethod"), + "has a public getter method that returns void"); + assertMethodFails(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithPrivateGetter"), + "is a getter method, but it is not public"); + assertMethodFails(MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, + PublicClass.class.getDeclaredMethod("publicReadMethod"), + "is suppose to be a public getter method, but its name (publicReadMethod) does not start with \"get\""); + } + + @Test + void fieldOrReadMethod() throws NoSuchFieldException, NoSuchMethodException { + assertFieldPasses(MemberAccessorType.FIELD_OR_READ_METHOD, PublicClass.class.getDeclaredField("publicField")); + assertFieldPasses(MemberAccessorType.FIELD_OR_READ_METHOD, + PublicClass.class.getDeclaredField("privateFieldWithGetter")); + assertFieldPasses(MemberAccessorType.FIELD_OR_READ_METHOD, + PublicClass.class.getDeclaredField("privateFieldWithGetterAndSetter")); + assertFieldPasses(MemberAccessorType.FIELD_OR_READ_METHOD, + PublicClass.class.getDeclaredField("privateFieldWithPrivateSetter")); + assertMethodPasses(MemberAccessorType.FIELD_OR_READ_METHOD, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithGetter")); + assertMethodPasses(MemberAccessorType.FIELD_OR_READ_METHOD, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithPrivateSetter")); + assertMethodPasses(MemberAccessorType.FIELD_OR_READ_METHOD, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithGetterAndSetter")); + assertMethodPasses(MemberAccessorType.FIELD_OR_READ_METHOD, + PublicClass.class.getDeclaredMethod("publicReadMethod")); + + assertFieldFails(MemberAccessorType.FIELD_OR_READ_METHOD, + PublicClass.class.getDeclaredField("privateFieldWithoutGetter"), + "is not public and does not have a public getter method"); + assertFieldFails(MemberAccessorType.FIELD_OR_READ_METHOD, + PublicClass.class.getDeclaredField("privateFieldWithPrivateGetter"), + "does not have a public getter method"); + assertMethodFails(MemberAccessorType.FIELD_OR_READ_METHOD, PublicClass.class.getDeclaredMethod("getVoidMethod"), + "is a public read method, but it returns void"); + assertMethodFails(MemberAccessorType.FIELD_OR_READ_METHOD, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithPrivateGetter"), + "is a read method, but it is not public."); + assertMethodFails(MemberAccessorType.FIELD_OR_READ_METHOD, + PublicClass.class.getDeclaredMethod("publicReadMethodWithParameter", String.class), + "is a public read method, but takes (1) parameters instead of none"); + } + + @Test + void fieldOrReadMethodWithOptionalParameter() throws NoSuchFieldException, NoSuchMethodException { + assertFieldPasses(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredField("publicField")); + assertFieldPasses(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredField("privateFieldWithGetter")); + assertFieldPasses(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredField("privateFieldWithGetterAndSetter")); + assertFieldPasses(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredField("privateFieldWithPrivateSetter")); + assertMethodPasses(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithGetter")); + assertMethodPasses(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithPrivateSetter")); + assertMethodPasses(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithGetterAndSetter")); + assertMethodPasses(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredMethod("publicReadMethod")); + assertMethodPasses(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredMethod("publicReadMethodWithParameter", String.class)); + + assertFieldFails(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredField("privateFieldWithoutGetter"), + "is not public and does not have a public getter method"); + assertFieldFails(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredField("privateFieldWithPrivateGetter"), + "does not have a public getter method"); + assertMethodFails(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredMethod("getVoidMethod"), + "is a public read method, but it returns void"); + assertMethodFails(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredMethod("getPrivateFieldWithPrivateGetter"), + "is a read method, but it is not public."); + assertMethodFails(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER, + PublicClass.class.getDeclaredMethod("publicReadMethodWithManyParameter", String.class, String.class), + "is a public read method, but takes (2) parameters instead of zero or one"); + } + + @Test + void voidMethod() throws NoSuchFieldException, NoSuchMethodException { + assertMethodPasses(MemberAccessorType.VOID_METHOD, PublicClass.class.getDeclaredMethod("getVoidMethod")); + + assertFieldFails(MemberAccessorType.VOID_METHOD, PublicClass.class.getDeclaredField("publicField"), + "must be a void method, but is a field instead"); + assertFieldFails(MemberAccessorType.VOID_METHOD, + PublicClass.class.getDeclaredField("privateFieldWithGetter"), + "must be a void method, but is a field instead"); + assertMethodFails(MemberAccessorType.VOID_METHOD, PublicClass.class.getDeclaredMethod("publicReadMethod"), + "must be a void method, but it returns (java.lang.String)"); + assertMethodFails(MemberAccessorType.VOID_METHOD, PublicClass.class.getDeclaredMethod("privateVoidMethod"), + "is a void method, but it is not public"); + } + + void assertFieldPasses(MemberAccessorType memberAccessorType, Field field) { + assertThatCode(() -> { + MemberAccessorValidator.verifyIsValidMember(null, field, memberAccessorType); + }).doesNotThrowAnyException(); + } + + void assertMethodPasses(MemberAccessorType memberAccessorType, Method method) { + assertThatCode(() -> { + MemberAccessorValidator.verifyIsValidMember(null, method, memberAccessorType); + }).doesNotThrowAnyException(); + } + + void assertFieldFails(MemberAccessorType memberAccessorType, Field field, String... expectedMessages) { + var prefix = "The field (%s) in class (%s)".formatted(field.getName(), field.getDeclaringClass().getCanonicalName()); + var allMessages = new ArrayList(); + allMessages.add(prefix); + allMessages.addAll(Arrays.asList(expectedMessages)); + + assertThatCode(() -> { + MemberAccessorValidator.verifyIsValidMember(null, field, memberAccessorType); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessageContainingAll(allMessages.toArray(new String[0])); + } + + void assertMethodFails(MemberAccessorType memberAccessorType, Method method, String... expectedMessages) { + var prefix = "The method (%s) in class (%s)".formatted(method.getName(), method.getDeclaringClass().getCanonicalName()); + var allMessages = new ArrayList(); + allMessages.add(prefix); + allMessages.addAll(Arrays.asList(expectedMessages)); + + assertThatCode(() -> { + MemberAccessorValidator.verifyIsValidMember(null, method, memberAccessorType); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessageContainingAll(allMessages.toArray(new String[0])); + } + + public static class PublicClass { + public String publicField; + private String privateFieldWithoutGetter; + private String privateFieldWithGetter; + private String privateFieldWithGetterAndSetter; + private String privateFieldWithPrivateGetter; + private String privateFieldWithPrivateSetter; + + public String getPrivateFieldWithGetter() { + return privateFieldWithGetter; + } + + public String getPrivateFieldWithGetterAndSetter() { + return privateFieldWithGetterAndSetter; + } + + public void setPrivateFieldWithGetterAndSetter(String privateFieldWithGetterAndSetter) { + this.privateFieldWithGetterAndSetter = privateFieldWithGetterAndSetter; + } + + private String getPrivateFieldWithPrivateGetter() { + return privateFieldWithPrivateGetter; + } + + public String getPrivateFieldWithPrivateSetter() { + return privateFieldWithPrivateSetter; + } + + private void setPrivateFieldWithPrivateSetter(String privateFieldWithPrivateSetter) { + this.privateFieldWithPrivateSetter = privateFieldWithPrivateSetter; + } + + public void getVoidMethod() { + // intentionally empty + } + + private void privateVoidMethod() { + // intentionally empty + } + + public String publicReadMethod() { + return null; + } + + private String privateReadMethod() { + return null; + } + + public String publicReadMethodWithParameter(String parameter) { + return parameter; + } + + public String publicReadMethodWithManyParameter(String parameter1, String parameter2) { + return parameter1; + } + } + + private static class PrivateClass { + public String publicField; + private String privateFieldWithGetter; + + public void publicVoidMethod() { + // Intentionally empty + } + + public String getPrivateFieldWithGetter() { + return privateFieldWithGetter; + } + } +} \ No newline at end of file diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java index f45cd023380..759f2b7ccf6 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java @@ -1,17 +1,12 @@ package ai.timefold.solver.core.impl.domain.common.accessor; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; -import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorFactory; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataValue; -import ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataDifferentGetterSetterVisibilityEntity; -import ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataInvalidGetterEntity; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; class ReflectionBeanPropertyMemberAccessorTest { @@ -30,39 +25,4 @@ void methodAnnotatedEntity() throws NoSuchMethodException { memberAccessor.executeSetter(e1, v2); assertThat(e1.getValue()).isSameAs(v2); } - - @Test - void getterSetterVisibilityDoesNotMatch() { - try (var gizmoMemberAccessorFactoryMock = Mockito.mockStatic(GizmoMemberAccessorFactory.class)) { - // Mock GizmoMemberAccessorFactory so MemberAccessorFactory think we are in a native image - gizmoMemberAccessorFactoryMock.when(() -> GizmoMemberAccessorFactory.isGizmoSupported(Mockito.any())) - .thenReturn(false); - gizmoMemberAccessorFactoryMock.when(() -> GizmoMemberAccessorFactory.getGeneratedClassName(Mockito.any())) - .thenCallRealMethod(); - - assertThatCode(() -> new ReflectionBeanPropertyMemberAccessor( - TestdataDifferentGetterSetterVisibilityEntity.class.getDeclaredMethod("getValue1"))) - .hasMessageContainingAll("getterMethod (getValue1)", - "has access modifier (public)", - "not match the setterMethod (setValue1)", - "access modifier (private)", - "on class (ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataDifferentGetterSetterVisibilityEntity)"); - assertThatCode(() -> new ReflectionBeanPropertyMemberAccessor( - TestdataDifferentGetterSetterVisibilityEntity.class.getDeclaredMethod("getValue2"))) - .hasMessageContainingAll("getterMethod (getValue2)", - "on class (%s)".formatted(TestdataDifferentGetterSetterVisibilityEntity.class.getCanonicalName()), - "is not public", - "having access modifier (package-private) instead."); - } - } - - @Test - void setterMissing() { - assertThatCode(() -> new ReflectionBeanPropertyMemberAccessor( - TestdataInvalidGetterEntity.class.getDeclaredMethod("getValueWithoutSetter"))) - .hasMessageContainingAll("getterMethod (getValueWithoutSetter)", - "does not have a matching setterMethod", - "on class (ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataInvalidGetterEntity)"); - } - } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodExtendedMemberAccessorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodExtendedMemberAccessorTest.java index 2c495945870..96f152aaefd 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodExtendedMemberAccessorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionMethodExtendedMemberAccessorTest.java @@ -6,21 +6,14 @@ import java.util.List; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; -import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorFactory; -import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.parameter.TestdataEntityProvidingWithParameterEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.parameter.TestdataEntityProvidingWithParameterSolution; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.parameter.inheritance.TestdataEntityProvidingEntityProvidingOnlyBaseAnnotatedExtendedSolution; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.parameter.inheritance.TestdataEntityProvidingOnlyBaseAnnotatedChildEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.parameter.inheritance.TestdataEntityProvidingOnlyBaseAnnotatedSolution; -import ai.timefold.solver.core.testdomain.valuerange.entityproviding.parameter.invalid.TestdataInvalidCountEntityProvidingWithParameterEntity; -import ai.timefold.solver.core.testdomain.valuerange.entityproviding.parameter.invalid.TestdataInvalidTypeEntityProvidingWithParameterEntity; -import ai.timefold.solver.core.testdomain.valuerange.entityproviding.parameter.invalid.TestdataInvalidTypeEntityProvidingWithParameterSolution; -import ai.timefold.solver.core.testdomain.valuerange.parameter.invalid.TestdataInvalidParameterSolution; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; class ReflectionMethodExtendedMemberAccessorTest { @@ -49,16 +42,17 @@ void methodAnnotatedEntityAndInheritance() throws NoSuchMethodException { var member = new ReflectionMethodExtendedMemberAccessor( TestdataEntityProvidingOnlyBaseAnnotatedChildEntity.class.getMethod("getValueList", TestdataEntityProvidingEntityProvidingOnlyBaseAnnotatedExtendedSolution.class)); - assertMemberWithInheritance(member, TestdataEntityProvidingEntityProvidingOnlyBaseAnnotatedExtendedSolution.class); + assertMemberWithInheritance(member, TestdataEntityProvidingEntityProvidingOnlyBaseAnnotatedExtendedSolution.class, + "getValueList"); var otherMember = new ReflectionMethodExtendedMemberAccessor( TestdataEntityProvidingOnlyBaseAnnotatedChildEntity.class.getMethod("getOtherValueList", TestdataEntityProvidingOnlyBaseAnnotatedSolution.class)); - assertMemberWithInheritance(otherMember, TestdataEntityProvidingOnlyBaseAnnotatedSolution.class); + assertMemberWithInheritance(otherMember, TestdataEntityProvidingOnlyBaseAnnotatedSolution.class, "getOtherValueList"); } - void assertMemberWithInheritance(ReflectionMethodExtendedMemberAccessor member, Class solutionClass) { + void assertMemberWithInheritance(ReflectionMethodExtendedMemberAccessor member, Class solutionClass, String memberName) { - assertThat(member.getName()).isEqualTo(member.getName()); + assertThat(member.getName()).isEqualTo(memberName); assertThat(member.getType()).isEqualTo(List.class); assertThat(member.getAnnotation(ValueRangeProvider.class)).isNotNull(); assertThat(member.getGetterMethodParameterType()).isEqualTo(solutionClass); @@ -74,61 +68,19 @@ void assertMemberWithInheritance(ReflectionMethodExtendedMemberAccessor member, assertThat(member.executeGetter(e1, s1)).isEqualTo(List.of(v2)); } - @Test - void invalidEntityReadMethodWithParameter() { - try (var gizmoMemberAccessorFactoryMock = Mockito.mockStatic(GizmoMemberAccessorFactory.class)) { - // Mock GizmoMemberAccessorFactory so MemberAccessorFactory think we are in a native image - gizmoMemberAccessorFactoryMock.when(() -> GizmoMemberAccessorFactory.isGizmoSupported(Mockito.any())) - .thenReturn(false); - gizmoMemberAccessorFactoryMock.when(() -> GizmoMemberAccessorFactory.getGeneratedClassName(Mockito.any())) - .thenCallRealMethod(); - - assertThatCode(TestdataInvalidTypeEntityProvidingWithParameterEntity::buildVariableDescriptorForValueRange) - .hasMessageContaining("The parameter type (%s)".formatted(TestdataSolution.class.getCanonicalName())) - .hasMessageContaining( - "of the method (getValueRange) must match the solution (%s)." - .formatted( - TestdataInvalidTypeEntityProvidingWithParameterSolution.class.getCanonicalName())); - assertThatCode(TestdataInvalidCountEntityProvidingWithParameterEntity::buildVariableDescriptorForValueRange) - .hasMessageContaining("The readMethod") - .hasMessageContaining("with a @%s annotation must have only one parameter" - .formatted(ValueRangeProvider.class.getSimpleName())); - } - } - - @Test - void invalidSolutionReadMethodWithParameter() { - try (var gizmoMemberAccessorFactoryMock = Mockito.mockStatic(GizmoMemberAccessorFactory.class)) { - // Mock GizmoMemberAccessorFactory so MemberAccessorFactory think we are in a native image - gizmoMemberAccessorFactoryMock.when(() -> GizmoMemberAccessorFactory.isGizmoSupported(Mockito.any())) - .thenReturn(false); - gizmoMemberAccessorFactoryMock.when(() -> GizmoMemberAccessorFactory.getGeneratedClassName(Mockito.any())) - .thenCallRealMethod(); - - assertThatCode(TestdataInvalidParameterSolution::buildSolutionDescriptor) - .hasMessageContainingAll( - "The readMethod (public java.util.List %s.getValueList(%s))" - .formatted(TestdataInvalidParameterSolution.class.getCanonicalName(), - TestdataInvalidParameterSolution.class.getCanonicalName())) - .hasMessageContainingAll( - " with a @%s annotation must not have any parameters ([class %s])." - .formatted(ValueRangeProvider.class.getSimpleName(), - TestdataInvalidParameterSolution.class.getCanonicalName())); - } - } - @Test void forbiddenEntityReadWithoutParameter() { assertThatCode(() -> new ReflectionMethodExtendedMemberAccessor( TestdataEntityProvidingWithParameterEntity.class.getMethod("getValueRange", - TestdataEntityProvidingWithParameterSolution.class), - true).executeGetter(new TestdataEntityProvidingWithParameterEntity())) + TestdataEntityProvidingWithParameterSolution.class)) + .executeGetter( + new TestdataEntityProvidingWithParameterEntity())) .hasMessageContainingAll( "Impossible state: the method executeGetter(Object) without parameter is not supported."); assertThatCode(() -> new ReflectionMethodExtendedMemberAccessor( TestdataEntityProvidingWithParameterEntity.class.getMethod("getValueRange", - TestdataEntityProvidingWithParameterSolution.class), - true).getGetterFunction()) + TestdataEntityProvidingWithParameterSolution.class)) + .getGetterFunction()) .hasMessageContainingAll( "Impossible state: the method getGetterFunction() is not supported."); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementorTest.java index d000559a9cd..0e2c3fc2322 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberAccessorImplementorTest.java @@ -10,7 +10,7 @@ import ai.timefold.solver.core.api.domain.entity.PlanningPin; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; -import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataValue; import ai.timefold.solver.core.testdomain.gizmo.GizmoTestdataEntity; @@ -20,7 +20,6 @@ import ai.timefold.solver.core.testdomain.valuerange.entityproviding.parameter.inheritance.TestdataEntityProvidingOnlyBaseAnnotatedChildEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.parameter.inheritance.TestdataEntityProvidingOnlyBaseAnnotatedSolution; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.parameter.invalid.TestdataInvalidCountEntityProvidingWithParameterEntity; -import ai.timefold.solver.core.testdomain.valuerange.parameter.invalid.TestdataInvalidParameterSolution; import org.junit.jupiter.api.Test; @@ -134,7 +133,7 @@ void testGeneratedMemberAccessorReturnVoid() throws NoSuchMethodException { var member = TestdataEntity.class.getMethod("updateValue"); var memberAccessor = GizmoMemberAccessorFactory.buildGizmoMemberAccessor(member, null, AccessorInfo.of( - MemberAccessorFactory.MemberAccessorType.VOID_METHOD), + MemberAccessorType.VOID_METHOD), new GizmoClassLoader()); var entity = new TestdataEntity(); @@ -153,41 +152,6 @@ void testGeneratedMemberAccessorReturnVoid() throws NoSuchMethodException { assertThat(memberAccessor.getSpeedNote()).isEqualTo("Fast access with generated bytecode"); } - @Test - void testThrowsWhenGetterMethodHasParameters() throws NoSuchMethodException { - var member = GizmoTestdataEntity.class.getMethod("methodWithParameters", String.class); - assertThatCode(() -> { - GizmoMemberAccessorImplementor.createAccessorFor(member, PlanningVariable.class, - AccessorInfo.withReturnValueAndNoArguments(), - new GizmoClassLoader()); - }).hasMessage("The getterMethod (methodWithParameters) with a PlanningVariable annotation " + - "must not have any parameters, but has parameters ([Ljava/lang/String;])."); - } - - @Test - void testThrowsWhenGetterMethodReturnVoid() throws NoSuchMethodException { - var member = GizmoTestdataEntity.class.getMethod("getVoid"); - assertThatCode(() -> { - GizmoMemberAccessorImplementor.createAccessorFor(member, PlanningVariable.class, - AccessorInfo.withReturnValueAndNoArguments(), - new GizmoClassLoader()); - }).hasMessageContainingAll("The getterMethod (getVoid)", - "with a @%s annotation".formatted(PlanningVariable.class.getSimpleName()), - "must have a non-void return type."); - } - - @Test - void testThrowsWhenReadMethodReturnVoid() throws NoSuchMethodException { - var member = GizmoTestdataEntity.class.getMethod("voidMethod"); - assertThatCode(() -> { - GizmoMemberAccessorImplementor.createAccessorFor(member, PlanningVariable.class, - AccessorInfo.withReturnValueAndNoArguments(), - new GizmoClassLoader()); - }).hasMessageContainingAll("The readMethod (voidMethod)", - "with a @%s annotation".formatted(PlanningVariable.class.getSimpleName()), - "must have a non-void return type."); - } - @Test void testGeneratedMemberAccessorForBooleanMethod() throws NoSuchMethodException { var member = GizmoTestdataEntity.class.getMethod("isPinned"); @@ -209,19 +173,6 @@ void testGeneratedMemberAccessorForBooleanMethod() throws NoSuchMethodException assertThat(memberAccessor.executeGetter(testdataEntity)).isEqualTo(true); } - @Test - void testThrowsWhenGetBooleanReturnsNonBoolean() throws NoSuchMethodException { - var member = GizmoTestdataEntity.class.getMethod("isAMethodThatHasABadName"); - assertThatCode( - () -> GizmoMemberAccessorImplementor.createAccessorFor(member, PlanningVariable.class, - AccessorInfo.of(MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD), - new GizmoClassLoader())) - .hasMessageContainingAll("The getterMethod (isAMethodThatHasABadName)", - "with a @%s annotation".formatted(PlanningVariable.class.getSimpleName()), - "must have a primitive boolean return type but returns (L%s;)".formatted( - String.class.getName().replace('.', '/'))); - } - @Test void testMethodAnnotatedEntity() throws NoSuchMethodException { var member = TestdataEntityProvidingWithParameterEntity.class.getMethod("getValueRange", @@ -285,19 +236,6 @@ void invalidEntityReadMethodWithParameter() throws NoSuchMethodException { "The getterMethod (getValueRange) must have only one parameter"); } - @Test - void invalidSolutionReadMethodWithParameter() throws NoSuchMethodException { - var member = TestdataInvalidParameterSolution.class.getMethod("getValueList", - TestdataInvalidParameterSolution.class); - assertThatCode(() -> GizmoMemberAccessorImplementor.createAccessorFor(member, ValueRangeProvider.class, - AccessorInfo.withReturnValueAndNoArguments(), - new GizmoClassLoader())) - .hasMessageContaining( - "The getterMethod (getValueList) with a ValueRangeProvider annotation must not have any parameters") - .hasMessageContaining( - "but has parameters ([Lai/timefold/solver/core/testdomain/valuerange/parameter/invalid/TestdataInvalidParameterSolution;])"); - } - @Test void testForbiddenEntityReadWithoutParameter() throws NoSuchMethodException { var member = TestdataEntityProvidingWithParameterEntity.class.getMethod("getValueRange", diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/lookup/AbstractLookupTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/lookup/AbstractLookupTest.java index 95335565945..f1def15bd9a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/lookup/AbstractLookupTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/lookup/AbstractLookupTest.java @@ -23,7 +23,7 @@ void setUpLookUpManager() { protected LookUpStrategyResolver createLookupStrategyResolver(LookUpStrategyType lookUpStrategyType) { DescriptorPolicy descriptorPolicy = new DescriptorPolicy(); descriptorPolicy.setMemberAccessorFactory(new MemberAccessorFactory()); - descriptorPolicy.setDomainAccessType(DomainAccessType.REFLECTION); + descriptorPolicy.setDomainAccessType(DomainAccessType.FORCE_REFLECTION); return new LookUpStrategyResolver(descriptorPolicy, lookUpStrategyType); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptorTest.java index a349f2d9196..01db6ec9d1a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptorTest.java @@ -81,7 +81,7 @@ void readMethodProblemFactCollectionProperty() { @Test void problemFactCollectionPropertyWithArgument() { - assertThatIllegalStateException().isThrownBy( + assertThatIllegalArgumentException().isThrownBy( TestdataProblemFactCollectionPropertyWithArgumentSolution::buildSolutionDescriptor); } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/solutionproperties/invalid/TestdataUnknownFactTypeSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/solutionproperties/invalid/TestdataUnknownFactTypeSolution.java index 18efcbe319b..7c6fc9e1db0 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/solutionproperties/invalid/TestdataUnknownFactTypeSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/solutionproperties/invalid/TestdataUnknownFactTypeSolution.java @@ -38,6 +38,10 @@ public List getValueList() { return valueList; } + public MyStringCollection getFacts() { + return facts; + } + public static interface MyStringCollection extends Collection { } diff --git a/docs/TODO.md b/docs/TODO.md index a463be529a7..df692a1d046 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -27,8 +27,9 @@ - [ ] BestSolutionChangedEvent now an interface. (All constructors were deprecated anyway.) - [ ] ProblemFactChange -> ProblemChange, migration script? - [ ] PinningFilter is gone, so is strengths and difficulties, and nullable. -- [ ] PlanningId moves from domain.lookup to domain.entity. +- [ ] PlanningId moves from domain.lookup to domain.common. - [ ] domain.lookup package is gone, so is lookup from PlanningSolution. - [ ] lookups no longer accept null values. +- [ ] `DomainAccessType` is gone, code uses GIZMO when possible Remove this file when done. \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/integration/config-properties.adoc b/docs/src/modules/ROOT/pages/integration/config-properties.adoc index 8bbe04db21b..c878c1e5905 100644 --- a/docs/src/modules/ROOT/pages/integration/config-properties.adoc +++ b/docs/src/modules/ROOT/pages/integration/config-properties.adoc @@ -44,18 +44,6 @@ Note that this is a feature of the xref:enterprise-edition/enterprise-edition.ad which is Timefold's commercial offering. See xref:enterprise-edition/enterprise-edition.adoc#multithreadedIncrementalSolving[multithreaded incremental solving]. -{property_prefix}timefold.solver.{solver_name_prefix}domain-access-type:: -How Timefold Solver should access the domain model. -See xref:using-timefold-solver/configuration.adoc#domainAccess[the domain access section] for more details. -ifeval::["{property_prefix}" == "quarkus."] -Defaults to `GIZMO`. -The other possible value is `REFLECTION`. -endif::[] -ifeval::["{property_prefix}" == ""] -Defaults to `REFLECTION`. -The other possible value is `GIZMO`. -endif::[] - {property_prefix}timefold.solver.{solver_name_prefix}nearby-distance-meter-class:: Enable the xref:enterprise-edition/enterprise-edition.adoc#nearbySelection[Nearby Selection] quick configuration. If the Nearby Selection distance meter class is specified, diff --git a/docs/src/modules/ROOT/pages/integration/integration.adoc b/docs/src/modules/ROOT/pages/integration/integration.adoc index 21cafdf98df..c10e8e5914a 100644 --- a/docs/src/modules/ROOT/pages/integration/integration.adoc +++ b/docs/src/modules/ROOT/pages/integration/integration.adoc @@ -289,8 +289,8 @@ include::config-properties.adoc[] The Quarkus integration allows the injection of several managed resources, including `SolverConfig`, `SolverFactory`, `SolverManager`, `SolutionManager`, `ConstraintVerifier` and `ConstraintMetaModel`. -The `SolverConfig` resource is constructed by reading the `application.properties` file and classpath. Therefore, Domain entities -(solution, entities, and constraint classes) and customized properties (`spent-limit`, `domain-access-type`, etc.) for the +The `SolverConfig` resource is constructed by reading the `application.properties` file and classpath. +Therefore, Domain entities (solution, entities, and constraint classes) and customized properties (`spent-limit`, etc.) for the planning problem are identified and loaded into the solver configuration. The available resouses can be injected as follows: @@ -647,8 +647,7 @@ include::config-properties.adoc[] The Spring Boot integration allows the injection of several managed resources, including `SolverConfig`, `SolverFactory`, `SolverManager`, `SolutionManager`, `ConstraintMetaModel` and `ConstraintVerifier`. -The `SolverConfig` resource is constructed by reading the `application.properties` file and classpath. Therefore, Domain entities -(solution, entities, and constraint classes) and customized properties (`spent-limit`, `domain-access-type`, etc.) for the +The `SolverConfig` resource is constructed by reading the `application.properties` file and classpath. Therefore, Domain entities (solution, entities, and constraint classes) and customized properties (`spent-limit`, etc.) for the planning problem are identified and loaded into the solver configuration. The available resouses can be injected as follows: diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/configuration.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/configuration.adoc index e56174c25db..81645507ae6 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/configuration.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/configuration.adoc @@ -148,33 +148,6 @@ Such a field does not need to be public. This manual focuses on the first manner, but every feature supports both, even if it's not explicitly mentioned. -[#domainAccess] -== Domain access - -Timefold Solver by default accesses your domain using reflection, which -will always work, but is slow compared to direct access. -Alternatively, you can configure Timefold Solver to access your domain -using Gizmo, which will generate bytecode that directly access the -fields/methods of your domain without reflection. However, it comes with some restrictions: - -* All fields in the domain must be public. -* The planning annotations can only be on public fields and - public getters. -* io.quarkus.gizmo:gizmo must be on the classpath. - -These restrictions do not apply when using Timefold Solver with Quarkus, -where Gizmo is the default domain access type. - -To use Gizmo outside of Quarkus, set the `domainAccessType` in the -Solver Configuration: - -[source,xml,options="nowrap"] ----- - - GIZMO - ----- - [#customPropertiesConfiguration] == Custom properties configuration diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java index 7ead7f84242..400cbb72284 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java @@ -44,7 +44,7 @@ import ai.timefold.solver.core.config.solver.SolverManagerConfig; import ai.timefold.solver.core.impl.domain.common.DomainAccessType; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; -import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType; import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.AccessorInfo; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.variable.declarative.RootVariableSource; @@ -1047,8 +1047,8 @@ private GeneratedGizmoClasses generateDomainAccessors(Map buildMethodAccessor(annotatedMember, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, classInfo, methodInfo, AccessorInfo.of((annotatedMember.name().equals(DotNames.VALUE_RANGE_PROVIDER)) - ? MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER - : MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD), + ? MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER + : MemberAccessorType.FIELD_OR_READ_METHOD), transformers); } default -> throw new IllegalStateException( @@ -1060,7 +1060,7 @@ private GeneratedGizmoClasses generateDomainAccessors(Map var targetMethodName = annotatedMember.value("targetMethodName").asString(); var methodInfo = classInfo.method(targetMethodName); buildMethodAccessor(null, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, classInfo, - methodInfo, AccessorInfo.of(MemberAccessorFactory.MemberAccessorType.VOID_METHOD), transformers); + methodInfo, AccessorInfo.of(MemberAccessorType.VOID_METHOD), transformers); } else if (annotatedMember.name().equals(DotNames.SHADOW_VARIABLE) && annotatedMember.value("supplierName") != null) { // The source method name also must be included @@ -1073,18 +1073,17 @@ private GeneratedGizmoClasses generateDomainAccessors(Map methodInfo = classInfo.method(targetMethodName, solutionType); } if (methodInfo == null) { - throw new IllegalArgumentException( - """ - @%s (%s) defines a supplierName (%s) that does not exist inside its declaring class (%s). - Maybe you included a parameter which is not a planning solution (%s)? - Maybe you misspelled the supplierName name?""" - .formatted(ShadowVariable.class.getSimpleName(), memberName, targetMethodName, - classInfo.name().toString(), solutionClassInfo.name().toString())); + throw new IllegalArgumentException(""" + @%s (%s) defines a supplierName (%s) that does not exist inside its declaring class (%s). + Maybe you included a parameter which is not a planning solution (%s)? + Maybe you misspelled the supplierName name?""" + .formatted(ShadowVariable.class.getSimpleName(), memberName, targetMethodName, + classInfo.name().toString(), solutionClassInfo.name().toString())); } buildMethodAccessor(annotatedMember, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput, classInfo, methodInfo, AccessorInfo - .of(MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER), + .of(MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER), transformers); } } @@ -1115,7 +1114,7 @@ Maybe you included a parameter which is not a planning solution (%s)? // Using REFLECTION domain access type so Timefold doesn't try to generate GIZMO code solverConfigMap.values().forEach(c -> { var solutionDescriptor = SolutionDescriptor.buildSolutionDescriptor( - c.getEnablePreviewFeatureSet(), DomainAccessType.REFLECTION, + c.getEnablePreviewFeatureSet(), DomainAccessType.FORCE_REFLECTION, c.getSolutionClass(), null, null, c.getEntityClassList()); gizmoSolutionClonerClassNameSet .add(entityEnhancer.generateSolutionCloner(solutionDescriptor, classOutput, indexView, transformers)); From 84ff78a7dfc8ec773bd9eba4c440be4d7c2a2d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Wed, 18 Feb 2026 08:15:40 +0100 Subject: [PATCH 3/3] Sonar --- .../accessor/MemberAccessorValidator.java | 145 +++++++++--------- .../accessor/gizmo/GizmoClassLoader.java | 4 +- .../accessor/gizmo/GizmoFieldHandler.java | 2 +- .../accessor/gizmo/GizmoMemberHandler.java | 7 +- 4 files changed, 79 insertions(+), 79 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorValidator.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorValidator.java index 3f5db988bcb..796c2d85640 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorValidator.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/MemberAccessorValidator.java @@ -69,29 +69,29 @@ private static void verifyGetterName(Method method, String messagePrefix) { .startsWith("is"))) { throw new IllegalArgumentException(""" %s is suppose to be a public getter method, but its name (%s) does not start with "get"%s. - Maybe add a "get" prefix to the method? - """.formatted(messagePrefix, method.getName(), - method.getReturnType().equals(boolean.class) ? " or \"is\"" : "")); + Maybe add a "get" prefix to the method?""" + .formatted(messagePrefix, method.getName(), + method.getReturnType().equals(boolean.class) ? " or \"is\"" : "")); } } private static void verifyFieldOrGetter(Member member, String messagePrefix) { - if (member instanceof Field field) { - verifyIsPublicFieldOrHasPublicGetter(field, messagePrefix); - } else if (member instanceof Method method) { - verifyGetterName(method, messagePrefix); - if (!Modifier.isPublic(method.getModifiers())) { - throw new IllegalArgumentException( - "%s is a getter method, but it is not public. Maybe make the method (%s) public?" - .formatted(messagePrefix, method.getName())); - } - if (method.getReturnType().equals(void.class)) { - throw new IllegalArgumentException( - "%s has a public getter method that returns void. Maybe make the method (%s) return a value instead?" - .formatted(messagePrefix, method.getName())); + switch (member) { + case Field field -> verifyIsPublicFieldOrHasPublicGetter(field, messagePrefix); + case Method method -> { + verifyGetterName(method, messagePrefix); + if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException( + "%s is a getter method, but it is not public. Maybe make the method (%s) public?" + .formatted(messagePrefix, method.getName())); + } + if (method.getReturnType().equals(void.class)) { + throw new IllegalArgumentException( + "%s has a public getter method that returns void. Maybe make the method (%s) return a value instead?" + .formatted(messagePrefix, method.getName())); + } } - } else { - throw new IllegalArgumentException("Unhandled member type (%s)." + default -> throw new IllegalArgumentException("Unhandled member type (%s)." .formatted(member.getClass().getCanonicalName())); } } @@ -120,72 +120,73 @@ private static void verifyIsPublicFieldOrHasPublicGetter(Field field, String mes private static void verifyIsPublicFieldOrHasReadMethod(Member member, String messagePrefix, boolean acceptOptionalParameter) { - if (member instanceof Field field) { - verifyIsPublicFieldOrHasPublicGetter(field, messagePrefix); - } else if (member instanceof Method method) { - if (!Modifier.isPublic(method.getModifiers())) { - throw new IllegalArgumentException( - "%s is a read method, but it is not public. Maybe make the method (%s) public?" - .formatted(messagePrefix, method.getName())); - } - if (method.getReturnType().equals(void.class)) { - throw new IllegalArgumentException( - "%s is a public read method, but it returns void. Maybe make the method (%s) return a value instead?" - .formatted(messagePrefix, method.getName())); - } + switch (member) { + case Field field -> verifyIsPublicFieldOrHasPublicGetter(field, messagePrefix); + case Method method -> { + if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException( + "%s is a read method, but it is not public. Maybe make the method (%s) public?" + .formatted(messagePrefix, method.getName())); + } + if (method.getReturnType().equals(void.class)) { + throw new IllegalArgumentException( + "%s is a public read method, but it returns void. Maybe make the method (%s) return a value instead?" + .formatted(messagePrefix, method.getName())); + } - if (acceptOptionalParameter) { - if (method.getParameterCount() > 1) { + if (acceptOptionalParameter) { + if (method.getParameterCount() > 1) { + throw new IllegalArgumentException(""" + %s is a public read method, but takes (%d) parameters instead of zero or one. + Maybe make the method (%s) take zero or one parameter(s)?""" + .formatted(messagePrefix, method.getParameterCount(), method.getName())); + } + } else if (method.getParameterCount() != 0) { throw new IllegalArgumentException(""" - %s is a public read method, but takes (%d) parameters instead of zero or one. - Maybe make the method (%s) take zero or one parameter(s)? - """.formatted(messagePrefix, method.getParameterCount(), method.getName())); + %s is a public read method, but takes (%d) parameters instead of none. + Maybe make the method (%s) take no parameters?""" + .formatted(messagePrefix, method.getParameterCount(), method.getName())); } - } else if (method.getParameterCount() != 0) { - throw new IllegalArgumentException(""" - %s is a public read method, but takes (%d) parameters instead of none. - Maybe make the method (%s) take no parameters? - """.formatted(messagePrefix, method.getParameterCount(), method.getName())); } - } else { - throw new IllegalArgumentException("Unhandled member type (%s)." + default -> throw new IllegalArgumentException("Unhandled member type (%s)." .formatted(member.getClass().getCanonicalName())); } } - private static void verifyIsPublicFieldOrHasPublicSetter(Member member, - String messagePrefix) { - if (member instanceof Field field) { - var getterMethod = ReflectionHelper.getGetterMethod(field.getDeclaringClass(), field.getName()); - var setterMethod = ReflectionHelper.getSetterMethod(field.getDeclaringClass(), field.getName()); - if (setterMethod == null) { - if (!Modifier.isPublic(field.getModifiers())) { - throw new IllegalArgumentException( - "%s does not have a setter method and is not public. Maybe add a public setter method?" - .formatted(messagePrefix)); + private static void verifyIsPublicFieldOrHasPublicSetter(Member member, String messagePrefix) { + switch (member) { + case Field field -> { + var getterMethod = ReflectionHelper.getGetterMethod(field.getDeclaringClass(), field.getName()); + var setterMethod = ReflectionHelper.getSetterMethod(field.getDeclaringClass(), field.getName()); + if (setterMethod == null) { + if (!Modifier.isPublic(field.getModifiers())) { + throw new IllegalArgumentException( + "%s does not have a setter method and is not public. Maybe add a public setter method?" + .formatted(messagePrefix)); + } + } else { + if (getterMethod == null) { + throw new IllegalArgumentException( + "%s has a setter method (%s) without a getter method. Maybe add a public getter method?" + .formatted(member, setterMethod.getName())); + } + verifyGetterSetterProperties(getterMethod, setterMethod, messagePrefix); } - } else { - if (getterMethod == null) { - throw new IllegalArgumentException( - "%s has a setter method (%s) without a getter method. Maybe add a public getter method?" - .formatted(member, setterMethod.getName())); + } + case Method getterMethod -> { + verifyGetterName(getterMethod, messagePrefix); + var memberName = ReflectionHelper.getGetterPropertyName(getterMethod); + var setterMethod = ReflectionHelper.getSetterMethod(getterMethod.getDeclaringClass(), + memberName); + if (setterMethod == null) { + throw new IllegalArgumentException(""" + %s requires both a public getter and a public setter but only have a public getter. + Maybe add a public setter for the member (%s)?""" + .formatted(messagePrefix, memberName)); } verifyGetterSetterProperties(getterMethod, setterMethod, messagePrefix); } - } else if (member instanceof Method getterMethod) { - verifyGetterName(getterMethod, messagePrefix); - var memberName = ReflectionHelper.getGetterPropertyName(getterMethod); - var setterMethod = ReflectionHelper.getSetterMethod(getterMethod.getDeclaringClass(), - memberName); - if (setterMethod == null) { - throw new IllegalArgumentException(""" - %s requires both a public getter and a public setter but only have a public getter. - Maybe add a public setter for the member (%s)? - """.formatted(messagePrefix, memberName)); - } - verifyGetterSetterProperties(getterMethod, setterMethod, messagePrefix); - } else { - throw new IllegalArgumentException("Unhandled member type (%s)." + default -> throw new IllegalArgumentException("Unhandled member type (%s)." .formatted(member.getClass().getCanonicalName())); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java index 2845a2697dc..06ddab357a8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java @@ -46,7 +46,7 @@ public String getName() { @Override public Class findClass(String name) throws ClassNotFoundException { - byte[] byteCode = getBytecodeFor(name); + var byteCode = getBytecodeFor(name); if (byteCode == null) { // Not a Gizmo generated class; load from context class loader. return Thread.currentThread().getContextClassLoader().loadClass(name); } else { // Gizmo generated class. @@ -54,7 +54,7 @@ public Class findClass(String name) throws ClassNotFoundException { } } - public synchronized byte[] getBytecodeFor(String className) { + public synchronized byte @Nullable [] getBytecodeFor(String className) { return classNameToBytecodeMap.get(className); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoFieldHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoFieldHandler.java index 6684292eeff..631af2b9c43 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoFieldHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoFieldHandler.java @@ -23,7 +23,7 @@ final class GizmoFieldHandler implements GizmoMemberHandler { private final @Nullable MethodDesc setterDescriptor; private final boolean canBeWritten; - GizmoFieldHandler(Class declaringClass, FieldDesc fieldDescriptor, boolean ignoreChecks, boolean canBeWritten) { + GizmoFieldHandler(Class declaringClass, FieldDesc fieldDescriptor, boolean canBeWritten) { this.declaringClass = declaringClass; this.fieldDescriptor = fieldDescriptor; var getterMethod = ReflectionHelper.getGetterMethod(declaringClass, fieldDescriptor.name()); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberHandler.java index 197c40c67ed..58541e3ecb1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoMemberHandler.java @@ -23,14 +23,13 @@ interface GizmoMemberHandler { * @param ignoreFinalChecks true if Quarkus will make the field non-final for us * @return never null */ - static GizmoMemberHandler of(Class declaringClass, String name, FieldDesc fieldDescriptor, - boolean ignoreFinalChecks) { + static GizmoMemberHandler of(Class declaringClass, String name, FieldDesc fieldDescriptor, boolean ignoreFinalChecks) { try { Field field = declaringClass.getField(name); return new GizmoFieldHandler(declaringClass, fieldDescriptor, - ignoreFinalChecks, ignoreFinalChecks || !Modifier.isFinal(field.getModifiers())); + ignoreFinalChecks || !Modifier.isFinal(field.getModifiers())); } catch (NoSuchFieldException e) { // The field is only used for its metadata and never actually called. - return new GizmoFieldHandler(declaringClass, fieldDescriptor, ignoreFinalChecks, false); + return new GizmoFieldHandler(declaringClass, fieldDescriptor, false); } }