Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
<dependency>
<groupId>io.quarkus.gizmo</groupId>
<artifactId>gizmo2</artifactId>
<!-- <optional>true</optional>-->
</dependency>

<!-- External dependencies -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,7 +72,6 @@
"monitoringConfig",
"solutionClass",
"entityClassList",
"domainAccessType",
"scoreDirectorFactoryConfig",
"terminationConfig",
"nearbyDistanceMeterClass",
Expand Down Expand Up @@ -228,7 +226,6 @@ public class SolverConfig extends AbstractConfig<SolverConfig> {

@XmlElement(name = "entityClass")
protected List<Class<?>> entityClassList = null;
protected DomainAccessType domainAccessType = null;
@XmlTransient
protected Map<String, MemberAccessor> gizmoMemberAccessorMap = null;
@XmlTransient
Expand Down Expand Up @@ -399,14 +396,6 @@ public void setEntityClassList(@Nullable List<Class<?>> 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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -7,25 +7,26 @@
* are accessed.
*/
public enum DomainAccessType {
/**
* Determine what domain access type to use automatically.
* <p>
* This is the default.
*/
AUTO,

/**
* Use reflection to read/write members (fields and methods) of the domain.
* <p>
* When used in a modulepath, the module must be open.
* When used in GraalVM, the domain must be open for reflection.
* <p>
* This is the default, except with timefold-solver-quarkus.
*/
REFLECTION,
FORCE_REFLECTION,

/**
* Use Gizmo generated bytecode to access members (fields and methods) to avoid reflection
* for additional performance.
* <p>
* With timefold-solver-quarkus, this bytecode is generated at build time
* and it supports planning annotations on non-public members too.
* <p>
* 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
FORCE_GIZMO
}
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand All @@ -30,10 +37,10 @@ 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
*/
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);
}
Expand All @@ -45,28 +52,31 @@ public static MemberAccessor buildMemberAccessor(Member member, MemberAccessorTy
* @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
*/
public static MemberAccessor buildMemberAccessor(Member member, MemberAccessorType memberAccessorType,
Class<? extends Annotation> annotationClass, DomainAccessType domainAccessType, ClassLoader classLoader) {
static MemberAccessor buildMemberAccessor(Member member, MemberAccessorType memberAccessorType,
@Nullable Class<? extends Annotation> annotationClass, DomainAccessType domainAccessType, ClassLoader classLoader) {
MemberAccessorValidator.verifyIsValidMember(annotationClass, member, memberAccessorType);
return switch (domainAccessType) {
case GIZMO -> GizmoMemberAccessorFactory.buildGizmoMemberAccessor(member, annotationClass,
AccessorInfo.of(memberAccessorType != MemberAccessorType.VOID_METHOD,
memberAccessorType == MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER),
case AUTO -> throw new IllegalStateException(
"Impossible state: called with %s (AUTO) instead of a resolved domain access type"
.formatted(DomainAccessType.class.getSimpleName()));
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);
};
}

private static MemberAccessor buildReflectiveMemberAccessor(Member member, MemberAccessorType memberAccessorType,
Class<? extends Annotation> annotationClass) {
@Nullable Class<? extends Annotation> annotationClass) {
return buildReflectiveMemberAccessor(member, memberAccessorType, annotationClass,
(AnnotatedElement) member);
}

private static MemberAccessor buildReflectiveMemberAccessor(Member member, MemberAccessorType memberAccessorType,
Class<? extends Annotation> annotationClass, AnnotatedElement annotatedElement) {
@Nullable Class<? extends Annotation> 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());
Expand Down Expand Up @@ -126,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."
Expand Down Expand Up @@ -154,6 +164,7 @@ private static MemberAccessor buildReflectiveMemberAccessor(Member member, Membe

private final Map<String, MemberAccessor> memberAccessorCache;
private final GizmoClassLoader gizmoClassLoader = new GizmoClassLoader();
private final boolean isGizmoSupported;

public MemberAccessorFactory() {
this(null);
Expand All @@ -162,12 +173,19 @@ 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(Map<String, MemberAccessor> memberAccessorMap) {
public MemberAccessorFactory(@Nullable Map<String, MemberAccessor> 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()) || gizmoClassLoader.isGizmoSupported();
LOGGER.trace("Using domain access type {} for member accessors.",
isGizmoSupported ? DomainAccessType.FORCE_GIZMO : DomainAccessType.FORCE_REFLECTION);
}

/**
Expand All @@ -180,10 +198,16 @@ public MemberAccessorFactory(Map<String, MemberAccessor> memberAccessorMap) {
* @return never null, new {@link MemberAccessor} instance unless already found in memberAccessorMap
*/
public MemberAccessor buildAndCacheMemberAccessor(Member member, MemberAccessorType memberAccessorType,
Class<? extends Annotation> annotationClass, DomainAccessType domainAccessType) {
@Nullable Class<? extends Annotation> annotationClass, DomainAccessType domainAccessType) {
String generatedClassName = GizmoMemberAccessorFactory.getGeneratedClassName(member);
if (domainAccessType == DomainAccessType.AUTO) {
domainAccessType = isGizmoSupported ? DomainAccessType.FORCE_GIZMO : DomainAccessType.FORCE_REFLECTION;
}

var finalDomainAccessType = domainAccessType;
return memberAccessorCache.computeIfAbsent(generatedClassName,
k -> MemberAccessorFactory.buildMemberAccessor(member, memberAccessorType, annotationClass, domainAccessType,
k -> MemberAccessorFactory.buildMemberAccessor(member, memberAccessorType, annotationClass,
finalDomainAccessType,
gizmoClassLoader));
}

Expand All @@ -198,33 +222,18 @@ 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.FORCE_GIZMO : DomainAccessType.FORCE_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() {
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading