diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index c3df777cf6..e569ab4ccd 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -43,6 +43,15 @@ jobs:
steps:
- uses: actions/checkout@v4
+ - uses: graalvm/setup-graalvm@v1
+ with:
+ java-version: '17'
+ distribution: 'graalvm-community'
+ set-java-home: 'false'
+ components: 'native-image'
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ cache: 'maven'
+
- uses: actions/setup-java@v4
with:
java-version: '17'
diff --git a/spring-integration/pom.xml b/spring-integration/pom.xml
index 4cd5a11e65..0c5c30a031 100644
--- a/spring-integration/pom.xml
+++ b/spring-integration/pom.xml
@@ -26,6 +26,7 @@
spring-boot-autoconfigure
spring-boot-starter
+ spring-boot-integration-test
diff --git a/spring-integration/spring-boot-autoconfigure/pom.xml b/spring-integration/spring-boot-autoconfigure/pom.xml
index 84f6cb8e61..0de50c0bb1 100644
--- a/spring-integration/spring-boot-autoconfigure/pom.xml
+++ b/spring-integration/spring-boot-autoconfigure/pom.xml
@@ -63,6 +63,13 @@
spring-boot-autoconfigure
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ true
+
+
org.springframework.boot
spring-boot-configuration-processor
@@ -78,11 +85,6 @@
spring-web
true
-
- com.fasterxml.jackson.core
- jackson-databind
- true
-
diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldBenchmarkAutoConfiguration.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldBenchmarkAutoConfiguration.java
index 9c58c92870..ea118c9f90 100644
--- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldBenchmarkAutoConfiguration.java
+++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldBenchmarkAutoConfiguration.java
@@ -27,7 +27,7 @@
import org.springframework.context.annotation.Lazy;
@Configuration
-@AutoConfigureAfter(TimefoldAutoConfiguration.class)
+@AutoConfigureAfter(TimefoldSolverAutoConfiguration.class)
@ConditionalOnClass({ PlannerBenchmarkFactory.class })
@ConditionalOnMissingBean({ PlannerBenchmarkFactory.class })
@EnableConfigurationProperties({ TimefoldProperties.class })
@@ -88,7 +88,7 @@ public PlannerBenchmarkConfig plannerBenchmarkConfig() {
}
if (timefoldProperties.getBenchmark() != null && timefoldProperties.getBenchmark().getSolver() != null) {
- TimefoldAutoConfiguration
+ TimefoldSolverAutoConfiguration
.applyTerminationProperties(benchmarkConfig.getInheritedSolverBenchmarkConfig().getSolverConfig(),
timefoldProperties.getBenchmark().getSolver().getTermination());
}
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
new file mode 100644
index 0000000000..e88b533c10
--- /dev/null
+++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java
@@ -0,0 +1,49 @@
+package ai.timefold.solver.spring.boot.autoconfigure;
+
+import java.util.Map;
+
+import ai.timefold.solver.core.config.solver.SolverConfig;
+
+import org.springframework.aot.generate.GenerationContext;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.ReflectionHints;
+import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
+import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
+
+public class TimefoldSolverAotContribution implements BeanFactoryInitializationAotContribution {
+ private final Map solverConfigMap;
+
+ public TimefoldSolverAotContribution(Map solverConfigMap) {
+ this.solverConfigMap = solverConfigMap;
+ }
+
+ /**
+ * Register a type for reflection, allowing introspection
+ * of its members at runtime in a native build.
+ */
+ 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.INVOKE_DECLARED_CONSTRUCTORS,
+ MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
+ MemberCategory.INVOKE_DECLARED_METHODS,
+ MemberCategory.INVOKE_PUBLIC_METHODS);
+ }
+
+ @Override
+ public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
+ ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection();
+ for (SolverConfig solverConfig : solverConfigMap.values()) {
+ solverConfig.visitReferencedClasses(type -> {
+ if (type != null) {
+ registerType(reflectionHints, type);
+ }
+ });
+ }
+ }
+}
diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotFactory.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotFactory.java
new file mode 100644
index 0000000000..5087078c25
--- /dev/null
+++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotFactory.java
@@ -0,0 +1,42 @@
+package ai.timefold.solver.spring.boot.autoconfigure;
+
+import java.io.StringReader;
+
+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.core.config.solver.SolverManagerConfig;
+import ai.timefold.solver.core.impl.io.jaxb.SolverConfigIO;
+import ai.timefold.solver.spring.boot.autoconfigure.config.SolverManagerProperties;
+import ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties;
+
+import org.springframework.boot.context.properties.bind.BindResult;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.context.EnvironmentAware;
+import org.springframework.core.env.Environment;
+
+public class TimefoldSolverAotFactory implements EnvironmentAware {
+ private TimefoldProperties timefoldProperties;
+
+ @Override
+ public void setEnvironment(Environment environment) {
+ // We need the environment to set run time properties of SolverFactory and SolverManager
+ BindResult result = Binder.get(environment).bind("timefold", TimefoldProperties.class);
+ this.timefoldProperties = result.orElseGet(TimefoldProperties::new);
+ }
+
+ public SolverManager solverManagerSupplier(String solverConfigXml) {
+ SolverFactory solverFactory = SolverFactory.create(solverConfigSupplier(solverConfigXml));
+ SolverManagerConfig solverManagerConfig = new SolverManagerConfig();
+ SolverManagerProperties solverManagerProperties = timefoldProperties.getSolverManager();
+ if (solverManagerProperties != null && solverManagerProperties.getParallelSolverCount() != null) {
+ solverManagerConfig.setParallelSolverCount(solverManagerProperties.getParallelSolverCount());
+ }
+ return SolverManager.create(solverFactory, solverManagerConfig);
+ }
+
+ public SolverConfig solverConfigSupplier(String solverConfigXml) {
+ SolverConfigIO solverConfigIO = new SolverConfigIO();
+ return solverConfigIO.read(new StringReader(solverConfigXml));
+ }
+}
diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAutoConfiguration.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java
similarity index 73%
rename from spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAutoConfiguration.java
rename to spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java
index dfed6a4df5..24284b5aae 100644
--- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAutoConfiguration.java
+++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java
@@ -2,6 +2,7 @@
import static java.util.stream.Collectors.joining;
+import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.HashMap;
@@ -10,7 +11,6 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
-import java.util.function.BiFunction;
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.entity.PlanningPin;
@@ -25,38 +25,31 @@
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
import ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable;
import ai.timefold.solver.core.api.domain.variable.ShadowVariable;
-import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.ScoreManager;
import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator;
import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator;
-import ai.timefold.solver.core.api.score.stream.Constraint;
-import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
-import ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType;
import ai.timefold.solver.core.api.solver.SolutionManager;
import ai.timefold.solver.core.api.solver.SolverFactory;
import ai.timefold.solver.core.api.solver.SolverManager;
import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig;
import ai.timefold.solver.core.config.solver.SolverConfig;
-import ai.timefold.solver.core.config.solver.SolverManagerConfig;
import ai.timefold.solver.core.config.solver.termination.TerminationConfig;
-import ai.timefold.solver.jackson.api.TimefoldJacksonModule;
-import ai.timefold.solver.spring.boot.autoconfigure.config.SolverManagerProperties;
+import ai.timefold.solver.core.impl.io.jaxb.SolverConfigIO;
import ai.timefold.solver.spring.boot.autoconfigure.config.SolverProperties;
import ai.timefold.solver.spring.boot.autoconfigure.config.TerminationProperties;
import ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties;
-import ai.timefold.solver.test.api.score.stream.ConstraintVerifier;
-import ai.timefold.solver.test.api.score.stream.MultiConstraintVerification;
-import ai.timefold.solver.test.api.score.stream.SingleConstraintVerification;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
-import org.springframework.beans.factory.BeanCreationException;
-import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
+import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
-import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -67,23 +60,19 @@
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
-import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
-import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
-import com.fasterxml.jackson.databind.Module;
-
-@Configuration
+@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SolverConfig.class, SolverFactory.class, ScoreManager.class, SolutionManager.class, SolverManager.class })
@ConditionalOnMissingBean({ SolverConfig.class, SolverFactory.class, ScoreManager.class, SolutionManager.class,
SolverManager.class })
@EnableConfigurationProperties({ TimefoldProperties.class })
-public class TimefoldAutoConfiguration
- implements BeanClassLoaderAware, ApplicationContextAware, EnvironmentAware, BeanFactoryPostProcessor {
+public class TimefoldSolverAutoConfiguration
+ implements BeanClassLoaderAware, ApplicationContextAware, EnvironmentAware, BeanFactoryInitializationAotProcessor,
+ BeanDefinitionRegistryPostProcessor {
- private static final Log LOG = LogFactory.getLog(TimefoldAutoConfiguration.class);
+ private static final Log LOG = LogFactory.getLog(TimefoldSolverAutoConfiguration.class);
private static final String DEFAULT_SOLVER_CONFIG_NAME = "getSolverConfig";
private static final Class extends Annotation>[] PLANNING_ENTITY_FIELD_ANNOTATIONS = new Class[] {
PlanningPin.class,
@@ -102,7 +91,7 @@ public class TimefoldAutoConfiguration
private ClassLoader beanClassLoader;
private TimefoldProperties timefoldProperties;
- protected TimefoldAutoConfiguration() {
+ protected TimefoldSolverAutoConfiguration() {
}
@Override
@@ -123,8 +112,7 @@ public void setEnvironment(Environment environment) {
this.timefoldProperties = result.orElseGet(TimefoldProperties::new);
}
- @Override
- public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+ private Map getSolverConfigMap() {
IncludeAbstractClassesEntityScanner entityScanner = new IncludeAbstractClassesEntityScanner(this.context);
if (!entityScanner.hasSolutionOrEntityClasses()) {
LOG.warn(
@@ -134,8 +122,7 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
Maybe move your planning solution and entity classes to your application class's (sub)package (or use @%s)."""
.formatted(PlanningSolution.class.getSimpleName(), PlanningEntity.class.getSimpleName(),
SpringBootApplication.class.getSimpleName(), EntityScan.class.getSimpleName()));
- beanFactory.registerSingleton(DEFAULT_SOLVER_CONFIG_NAME, new SolverConfig(beanClassLoader));
- return;
+ return Map.of();
}
Map solverConfigMap = new HashMap<>();
// Step 1 - create all SolverConfig
@@ -156,20 +143,53 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
// Step 3 - load all additional information per SolverConfig
solverConfigMap.forEach(
(solverName, solverConfig) -> loadSolverConfig(entityScanner, timefoldProperties, solverName, solverConfig));
+ return solverConfigMap;
+ }
+
+ @Override
+ public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
+ Map solverConfigMap = getSolverConfigMap();
+ return new TimefoldSolverAotContribution(solverConfigMap);
+ }
+
+ @Override
+ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
+ Map solverConfigMap = getSolverConfigMap();
+ SolverConfigIO solverConfigIO = new SolverConfigIO();
+ registry.registerBeanDefinition(TimefoldSolverAotFactory.class.getName(),
+ new RootBeanDefinition(TimefoldSolverAotFactory.class));
+ if (solverConfigMap.isEmpty()) {
+ RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(SolverConfig.class);
+ rootBeanDefinition.setFactoryBeanName(TimefoldSolverAotFactory.class.getName());
+ rootBeanDefinition.setFactoryMethodName("solverConfigSupplier");
+ StringWriter solverXmlOutput = new StringWriter();
+ solverConfigIO.write(new SolverConfig(), solverXmlOutput);
+ rootBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(
+ solverXmlOutput.toString());
+ registry.registerBeanDefinition(DEFAULT_SOLVER_CONFIG_NAME, rootBeanDefinition);
+ return;
+ }
if (timefoldProperties.getSolver() == null || timefoldProperties.getSolver().size() == 1) {
- beanFactory.registerSingleton(DEFAULT_SOLVER_CONFIG_NAME, solverConfigMap.values().iterator().next());
+ RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(SolverConfig.class);
+ rootBeanDefinition.setFactoryBeanName(TimefoldSolverAotFactory.class.getName());
+ rootBeanDefinition.setFactoryMethodName("solverConfigSupplier");
+ StringWriter solverXmlOutput = new StringWriter();
+ solverConfigIO.write(solverConfigMap.values().iterator().next(), solverXmlOutput);
+ rootBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(
+ solverXmlOutput.toString());
+ registry.registerBeanDefinition(DEFAULT_SOLVER_CONFIG_NAME, rootBeanDefinition);
} else {
// Only SolverManager can be injected for multiple solver configurations
solverConfigMap.forEach((solverName, solverConfig) -> {
- SolverFactory> solverFactory = SolverFactory.create(solverConfig);
-
- SolverManagerConfig solverManagerConfig = new SolverManagerConfig();
- SolverManagerProperties solverManagerProperties = timefoldProperties.getSolverManager();
- if (solverManagerProperties != null && solverManagerProperties.getParallelSolverCount() != null) {
- solverManagerConfig.setParallelSolverCount(solverManagerProperties.getParallelSolverCount());
- }
- beanFactory.registerSingleton(solverName, SolverManager.create(solverFactory, solverManagerConfig));
+ RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(SolverManager.class);
+ rootBeanDefinition.setFactoryBeanName(TimefoldSolverAotFactory.class.getName());
+ rootBeanDefinition.setFactoryMethodName("solverManagerSupplier");
+ StringWriter solverXmlOutput = new StringWriter();
+ solverConfigIO.write(solverConfig, solverXmlOutput);
+ rootBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(
+ solverXmlOutput.toString());
+ registry.registerBeanDefinition(solverName, rootBeanDefinition);
});
}
}
@@ -256,7 +276,8 @@ protected void applyScoreDirectorFactoryProperties(IncludeAbstractClassesEntityS
}
}
- private ScoreDirectorFactoryConfig defaultScoreDirectoryFactoryConfig(IncludeAbstractClassesEntityScanner entityScanner) {
+ private static ScoreDirectorFactoryConfig
+ defaultScoreDirectoryFactoryConfig(IncludeAbstractClassesEntityScanner entityScanner) {
ScoreDirectorFactoryConfig scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig();
scoreDirectorFactoryConfig
.setEasyScoreCalculatorClass(entityScanner.findFirstImplementingClass(EasyScoreCalculator.class));
@@ -287,14 +308,7 @@ static void applyTerminationProperties(SolverConfig solverConfig, TerminationPro
}
}
- private void failInjectionWithMultipleSolvers(String resourceName) {
- if (timefoldProperties.getSolver() != null && timefoldProperties.getSolver().size() > 1) {
- throw new BeanCreationException(
- "No qualifying bean of type '%s' available".formatted(resourceName));
- }
- }
-
- private void assertNoMemberAnnotationWithoutClassAnnotation(IncludeAbstractClassesEntityScanner entityScanner) {
+ private static void assertNoMemberAnnotationWithoutClassAnnotation(IncludeAbstractClassesEntityScanner entityScanner) {
List> timefoldFieldAnnotationList =
entityScanner.findClassesWithAnnotation(PLANNING_ENTITY_FIELD_ANNOTATIONS);
List> entityList = entityScanner.findEntityClassList();
@@ -311,7 +325,7 @@ The classes ([%s]) do not have the %s annotation, even though they contain prope
}
}
- private void assertSolverConfigSolutionClasses(IncludeAbstractClassesEntityScanner entityScanner,
+ private static void assertSolverConfigSolutionClasses(IncludeAbstractClassesEntityScanner entityScanner,
Map solverConfigMap) {
// Validate the solution class
// No solution class
@@ -365,7 +379,7 @@ Some solver configs (%s) don't specify a %s class, yet there are multiple availa
}
}
- private void assertSolverConfigEntityClasses(IncludeAbstractClassesEntityScanner entityScanner) {
+ private static void assertSolverConfigEntityClasses(IncludeAbstractClassesEntityScanner entityScanner) {
// No Entity class
String emptyListErrorMessage = """
No classes were found with a @%s annotation.
@@ -378,7 +392,7 @@ private void assertSolverConfigEntityClasses(IncludeAbstractClassesEntityScanner
assertTargetClasses(entityScanner.findEntityClassList(), PlanningEntity.class.getSimpleName());
}
- private void assertSolverConfigConstraintClasses(
+ private static void assertSolverConfigConstraintClasses(
IncludeAbstractClassesEntityScanner entityScanner, Map solverConfigMap) {
List> simpleScoreClassList =
entityScanner.findImplementingClassList(EasyScoreCalculator.class);
@@ -386,7 +400,7 @@ private void assertSolverConfigConstraintClasses(
entityScanner.findImplementingClassList(ConstraintProvider.class);
List> incrementalScoreClassList =
entityScanner.findImplementingClassList(IncrementalScoreCalculator.class);
- // No score classes
+ // No score calculators
if (simpleScoreClassList.isEmpty() && constraintScoreClassList.isEmpty()
&& incrementalScoreClassList.isEmpty()) {
throw new IllegalStateException(
@@ -399,8 +413,17 @@ private void assertSolverConfigConstraintClasses(
ConstraintProvider.class.getSimpleName(), SpringBootApplication.class.getSimpleName(),
ConstraintProvider.class.getSimpleName(), EntityScan.class.getSimpleName()));
}
- // Multiple classes and single solver
- String errorMessage = "Multiple score classes classes (%s) that implements %s were found in the classpath.";
+ assertSolverConfigsSpecifyScoreCalculatorWhenAmbigious(solverConfigMap, simpleScoreClassList, constraintScoreClassList,
+ incrementalScoreClassList);
+ assertNoUnusedScoreClasses(solverConfigMap, simpleScoreClassList, constraintScoreClassList, incrementalScoreClassList);
+ }
+
+ private static void assertSolverConfigsSpecifyScoreCalculatorWhenAmbigious(Map solverConfigMap,
+ List> simpleScoreClassList,
+ List> constraintScoreClassList,
+ List> incrementalScoreClassList) {
+ // Single solver, multiple score calculators
+ String errorMessage = "Multiple score calculator classes (%s) that implements %s were found in the classpath.";
if (simpleScoreClassList.size() > 1 && solverConfigMap.size() == 1) {
throw new IllegalStateException(errorMessage.formatted(
simpleScoreClassList.stream().map(Class::getSimpleName).collect(joining(", ")),
@@ -416,11 +439,12 @@ private void assertSolverConfigConstraintClasses(
incrementalScoreClassList.stream().map(Class::getSimpleName).collect(joining(", ")),
IncrementalScoreCalculator.class.getSimpleName()));
}
- // Multiple classes and at least one solver config does not specify the score class
- errorMessage = """
- Some solver configs (%s) don't specify a %s score class, yet there are multiple available (%s) on the classpath.
- Maybe set the XML config file to the related solver configs, or add the missing score classes to the XML files,
- or remove the unnecessary score classes from the classpath.""";
+ // Multiple solvers, multiple score calculators
+ errorMessage =
+ """
+ Some solver configs (%s) don't specify a %s score calculator class, yet there are multiple available (%s) on the classpath.
+ Maybe set the XML config file to the related solver configs, or add the missing score calculator to the XML files,
+ or remove the unnecessary score calculator from the classpath.""";
List solverConfigWithoutConstraintClassList = solverConfigMap.entrySet().stream()
.filter(e -> e.getValue().getScoreDirectorFactoryConfig() == null
|| e.getValue().getScoreDirectorFactoryConfig().getEasyScoreCalculatorClass() == null)
@@ -454,7 +478,13 @@ Some solver configs (%s) don't specify a %s score class, yet there are multiple
IncrementalScoreCalculator.class.getSimpleName(),
incrementalScoreClassList.stream().map(Class::getSimpleName).collect(joining(", "))));
}
- // Unused score classes
+ }
+
+ private static void assertNoUnusedScoreClasses(Map solverConfigMap,
+ List> simpleScoreClassList,
+ List> constraintScoreClassList,
+ List> incrementalScoreClassList) {
+ String errorMessage;
List solverConfigWithUnusedSolutionClassList = simpleScoreClassList.stream()
.map(Class::getName)
.filter(className -> solverConfigMap.values().stream()
@@ -494,7 +524,8 @@ Some solver configs (%s) don't specify a %s score class, yet there are multiple
}
}
- private void assertEmptyInstances(IncludeAbstractClassesEntityScanner entityScanner, Class extends Annotation> clazz,
+ private static void assertEmptyInstances(IncludeAbstractClassesEntityScanner entityScanner,
+ Class extends Annotation> clazz,
String errorMessage) {
try {
Collection> classInstanceCollection = entityScanner.scan(clazz);
@@ -506,7 +537,7 @@ private void assertEmptyInstances(IncludeAbstractClassesEntityScanner entityScan
}
}
- private void assertTargetClasses(Collection> targetCollection, String targetAnnotation) {
+ private static void assertTargetClasses(Collection> targetCollection, String targetAnnotation) {
List invalidClasses = targetCollection.stream()
.filter(target -> target.isRecord() || target.isEnum() || target.isPrimitive())
.map(Class::getSimpleName)
@@ -518,135 +549,4 @@ private void assertTargetClasses(Collection> targetCollection, String t
targetAnnotation));
}
}
-
- @Bean
- @Lazy
- public TimefoldSolverBannerBean getBanner() {
- return new TimefoldSolverBannerBean();
- }
-
- @Bean
- @Lazy
- @ConditionalOnMissingBean
- public SolverFactory getSolverFactory() {
- failInjectionWithMultipleSolvers(SolverFactory.class.getName());
- SolverConfig solverConfig = context.getBean(SolverConfig.class);
- if (solverConfig == null || solverConfig.getSolutionClass() == null) {
- return null;
- }
- return SolverFactory.create(solverConfig);
- }
-
- @Bean
- @Lazy
- @ConditionalOnMissingBean
- public SolverManager solverManager(SolverFactory solverFactory) {
- // TODO supply ThreadFactory
- if (solverFactory == null) {
- return null;
- }
- SolverManagerConfig solverManagerConfig = new SolverManagerConfig();
- SolverManagerProperties solverManagerProperties = timefoldProperties.getSolverManager();
- if (solverManagerProperties != null && solverManagerProperties.getParallelSolverCount() != null) {
- solverManagerConfig.setParallelSolverCount(solverManagerProperties.getParallelSolverCount());
- }
- return SolverManager.create(solverFactory, solverManagerConfig);
- }
-
- @Bean
- @Lazy
- @ConditionalOnMissingBean
- @Deprecated(forRemoval = true)
- public > ScoreManager scoreManager() {
- failInjectionWithMultipleSolvers(ScoreManager.class.getName());
- SolverFactory solverFactory = context.getBean(SolverFactory.class);
- if (solverFactory == null) {
- return null;
- }
- return ScoreManager.create(solverFactory);
- }
-
- @Bean
- @Lazy
- @ConditionalOnMissingBean
- public > SolutionManager solutionManager() {
- failInjectionWithMultipleSolvers(SolutionManager.class.getName());
- SolverFactory solverFactory = context.getBean(SolverFactory.class);
- if (solverFactory == null) {
- return null;
- }
- return SolutionManager.create(solverFactory);
- }
-
- // @Bean wrapped by static class to avoid classloading issues if dependencies are absent
- @ConditionalOnClass({ ConstraintVerifier.class })
- @ConditionalOnMissingBean({ ConstraintVerifier.class })
- @AutoConfigureAfter(TimefoldAutoConfiguration.class)
- class TimefoldConstraintVerifierConfiguration {
-
- private final ApplicationContext context;
-
- protected TimefoldConstraintVerifierConfiguration(ApplicationContext context) {
- this.context = context;
- }
-
- @Bean
- @Lazy
- @SuppressWarnings("unchecked")
-
- ConstraintVerifier constraintVerifier() {
- // Using SolverConfig as an injected parameter here leads to an injection failure on an empty app,
- // so we need to get the SolverConfig from context
- failInjectionWithMultipleSolvers(ConstraintProvider.class.getName());
- SolverConfig solverConfig;
- try {
- solverConfig = context.getBean(SolverConfig.class);
- } catch (BeansException exception) {
- solverConfig = null;
- }
-
- ScoreDirectorFactoryConfig scoreDirectorFactoryConfig =
- (solverConfig != null) ? solverConfig.getScoreDirectorFactoryConfig() : null;
- if (scoreDirectorFactoryConfig == null || scoreDirectorFactoryConfig.getConstraintProviderClass() == null) {
- // Return a mock ConstraintVerifier so not having ConstraintProvider doesn't crash tests
- // (Cannot create custom condition that checks SolverConfig, since that
- // requires TimefoldAutoConfiguration to have a no-args constructor)
- final String noConstraintProviderErrorMsg = (scoreDirectorFactoryConfig != null)
- ? "Cannot provision a ConstraintVerifier because there is no ConstraintProvider class."
- : "Cannot provision a ConstraintVerifier because there is no PlanningSolution or PlanningEntity classes.";
- return new ConstraintVerifier<>() {
- @Override
- public ConstraintVerifier
- withConstraintStreamImplType(ConstraintStreamImplType constraintStreamImplType) {
- throw new UnsupportedOperationException(noConstraintProviderErrorMsg);
- }
-
- @Override
- public SingleConstraintVerification
- verifyThat(BiFunction constraintFunction) {
- throw new UnsupportedOperationException(noConstraintProviderErrorMsg);
- }
-
- @Override
- public MultiConstraintVerification verifyThat() {
- throw new UnsupportedOperationException(noConstraintProviderErrorMsg);
- }
- };
- }
-
- return ConstraintVerifier.create(solverConfig);
- }
- }
-
- // @Bean wrapped by static class to avoid classloading issues if dependencies are absent
- @Configuration(proxyBeanMethods = false)
- @ConditionalOnClass({ Jackson2ObjectMapperBuilder.class, Score.class })
- static class TimefoldJacksonConfiguration {
-
- @Bean
- Module jacksonModule() {
- return TimefoldJacksonModule.createModule();
- }
-
- }
}
diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverBeanFactory.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverBeanFactory.java
new file mode 100644
index 0000000000..ff5411f0c8
--- /dev/null
+++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverBeanFactory.java
@@ -0,0 +1,210 @@
+package ai.timefold.solver.spring.boot.autoconfigure;
+
+import java.util.function.BiFunction;
+
+import ai.timefold.solver.core.api.score.Score;
+import ai.timefold.solver.core.api.score.ScoreManager;
+import ai.timefold.solver.core.api.score.stream.Constraint;
+import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
+import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
+import ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType;
+import ai.timefold.solver.core.api.solver.SolutionManager;
+import ai.timefold.solver.core.api.solver.SolverFactory;
+import ai.timefold.solver.core.api.solver.SolverManager;
+import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig;
+import ai.timefold.solver.core.config.solver.SolverConfig;
+import ai.timefold.solver.core.config.solver.SolverManagerConfig;
+import ai.timefold.solver.jackson.api.TimefoldJacksonModule;
+import ai.timefold.solver.spring.boot.autoconfigure.config.SolverManagerProperties;
+import ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties;
+import ai.timefold.solver.test.api.score.stream.ConstraintVerifier;
+import ai.timefold.solver.test.api.score.stream.MultiConstraintVerification;
+import ai.timefold.solver.test.api.score.stream.SingleConstraintVerification;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.bind.BindResult;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.EnvironmentAware;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.core.env.Environment;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+
+import com.fasterxml.jackson.databind.Module;
+
+/**
+ * Must be seperated from {@link TimefoldSolverAutoConfiguration} since
+ * {@link TimefoldSolverAutoConfiguration} will not be available at runtime
+ * for a native image (since it is a {@link BeanFactoryInitializationAotProcessor}/
+ * {@link BeanFactoryPostProcessor}).
+ */
+@Configuration
+public class TimefoldSolverBeanFactory implements ApplicationContextAware, EnvironmentAware {
+ private ApplicationContext context;
+ private TimefoldProperties timefoldProperties;
+
+ @Override
+ public void setApplicationContext(ApplicationContext context) throws BeansException {
+ this.context = context;
+ }
+
+ @Override
+ public void setEnvironment(Environment environment) {
+ // We need the environment to set run time properties of SolverFactory and SolverManager
+ BindResult result = Binder.get(environment).bind("timefold", TimefoldProperties.class);
+ this.timefoldProperties = result.orElseGet(TimefoldProperties::new);
+ }
+
+ private void failInjectionWithMultipleSolvers(String resourceName) {
+ if (timefoldProperties.getSolver() != null && timefoldProperties.getSolver().size() > 1) {
+ throw new BeanCreationException(
+ "No qualifying bean of type '%s' available".formatted(resourceName));
+ }
+ }
+
+ @Bean
+ @Lazy
+ public TimefoldSolverBannerBean getBanner() {
+ return new TimefoldSolverBannerBean();
+ }
+
+ @Bean
+ @Lazy
+ @ConditionalOnMissingBean
+ public SolverFactory getSolverFactory() {
+ failInjectionWithMultipleSolvers(SolverFactory.class.getName());
+ SolverConfig solverConfig = context.getBean(SolverConfig.class);
+ if (solverConfig.getSolutionClass() == null) {
+ return null;
+ }
+ return SolverFactory.create(solverConfig);
+ }
+
+ @Bean
+ @Lazy
+ @ConditionalOnMissingBean
+ public SolverManager solverManager(SolverFactory solverFactory) {
+ // TODO supply ThreadFactory
+ if (solverFactory == null) {
+ return null;
+ }
+ SolverManagerConfig solverManagerConfig = new SolverManagerConfig();
+ SolverManagerProperties solverManagerProperties = timefoldProperties.getSolverManager();
+ if (solverManagerProperties != null && solverManagerProperties.getParallelSolverCount() != null) {
+ solverManagerConfig.setParallelSolverCount(solverManagerProperties.getParallelSolverCount());
+ }
+ return SolverManager.create(solverFactory, solverManagerConfig);
+ }
+
+ @Bean
+ @Lazy
+ @ConditionalOnMissingBean
+ @Deprecated(forRemoval = true)
+ /**
+ * @deprecated Use {@link SolutionManager} instead.
+ */
+ public > ScoreManager scoreManager() {
+ failInjectionWithMultipleSolvers(ScoreManager.class.getName());
+ SolverFactory solverFactory = context.getBean(SolverFactory.class);
+ return ScoreManager.create(solverFactory);
+ }
+
+ @Bean
+ @Lazy
+ @ConditionalOnMissingBean
+ public > SolutionManager solutionManager() {
+ failInjectionWithMultipleSolvers(SolutionManager.class.getName());
+ SolverFactory solverFactory = context.getBean(SolverFactory.class);
+ return SolutionManager.create(solverFactory);
+ }
+
+ // @Bean wrapped by static class to avoid classloading issues if dependencies are absent
+ @ConditionalOnClass({ ConstraintVerifier.class })
+ @ConditionalOnMissingBean({ ConstraintVerifier.class })
+ @AutoConfigureAfter(TimefoldSolverAutoConfiguration.class)
+ class TimefoldConstraintVerifierConfiguration {
+
+ private final ApplicationContext context;
+
+ protected TimefoldConstraintVerifierConfiguration(ApplicationContext context) {
+ this.context = context;
+ }
+
+ private static class UnsupportedConstraintVerifier
+ implements ConstraintVerifier {
+ final String errorMessage;
+
+ public UnsupportedConstraintVerifier(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ @Override
+ public ConstraintVerifier
+ withConstraintStreamImplType(ConstraintStreamImplType constraintStreamImplType) {
+ throw new UnsupportedOperationException(errorMessage);
+ }
+
+ @Override
+ public SingleConstraintVerification
+ verifyThat(BiFunction constraintFunction) {
+ throw new UnsupportedOperationException(errorMessage);
+ }
+
+ @Override
+ public MultiConstraintVerification verifyThat() {
+ throw new UnsupportedOperationException(errorMessage);
+ }
+ }
+
+ @Bean
+ @Lazy
+ @SuppressWarnings("unchecked")
+
+ ConstraintVerifier constraintVerifier() {
+ // Using SolverConfig as an injected parameter here leads to an injection failure on an empty app,
+ // so we need to get the SolverConfig from context
+ failInjectionWithMultipleSolvers(ConstraintProvider.class.getName());
+ SolverConfig solverConfig;
+ try {
+ solverConfig = context.getBean(SolverConfig.class);
+ } catch (BeansException exception) {
+ solverConfig = null;
+ }
+
+ ScoreDirectorFactoryConfig scoreDirectorFactoryConfig =
+ (solverConfig != null) ? solverConfig.getScoreDirectorFactoryConfig() : null;
+ if (scoreDirectorFactoryConfig == null || scoreDirectorFactoryConfig.getConstraintProviderClass() == null) {
+ // Return a mock ConstraintVerifier so not having ConstraintProvider doesn't crash tests
+ // (Cannot create custom condition that checks SolverConfig, since that
+ // requires TimefoldSolverAutoConfiguration to have a no-args constructor)
+ final String noConstraintProviderErrorMsg = (scoreDirectorFactoryConfig != null)
+ ? "Cannot provision a ConstraintVerifier because there is no ConstraintProvider class."
+ : "Cannot provision a ConstraintVerifier because there is no PlanningSolution or PlanningEntity classes.";
+ return new UnsupportedConstraintVerifier<>(noConstraintProviderErrorMsg);
+ }
+
+ return ConstraintVerifier.create(solverConfig);
+ }
+ }
+
+ // @Bean wrapped by static class to avoid classloading issues if dependencies are absent
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass({ Jackson2ObjectMapperBuilder.class, Score.class })
+ static class TimefoldJacksonConfiguration {
+
+ @Bean
+ Module jacksonModule() {
+ return TimefoldJacksonModule.createModule();
+ }
+
+ }
+}
diff --git a/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/proxy-config.json b/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/proxy-config.json
new file mode 100644
index 0000000000..67b84579d7
--- /dev/null
+++ b/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/proxy-config.json
@@ -0,0 +1,8 @@
+[
+ {
+ "interfaces": [
+ "java.lang.Deprecated",
+ "org.glassfish.jaxb.core.v2.model.annotation.Locatable"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/reflect-config.json b/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/reflect-config.json
new file mode 100644
index 0000000000..12579138e6
--- /dev/null
+++ b/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/reflect-config.json
@@ -0,0 +1,801 @@
+[
+ {
+ "name": "java.util.ArrayList",
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbCustomPropertiesAdapter",
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbDurationAdapter",
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbLocaleAdapter",
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbOffsetDateTimeAdapter",
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.api.domain.common.DomainAccessType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.AbstractConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.constructionheuristic.decider.forager.ConstructionHeuristicForagerConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.constructionheuristic.decider.forager.ConstructionHeuristicPickEarlyType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.constructionheuristic.placer.EntityPlacerConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.constructionheuristic.placer.PooledEntityPlacerConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.constructionheuristic.placer.QueuedEntityPlacerConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.constructionheuristic.placer.QueuedValuePlacerConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.exhaustivesearch.ExhaustiveSearchPhaseConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.exhaustivesearch.ExhaustiveSearchType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.exhaustivesearch.NodeExplorationType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.SelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.common.nearby.NearbySelectionConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.common.nearby.NearbySelectionDistributionType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.entity.pillar.PillarSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.list.DestinationSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.list.SubListSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.composite.CartesianProductMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveIteratorFactoryConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveListFactoryConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.AbstractPillarMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.SubPillarType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.SwapMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainSwapMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.TailChainSwapMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListChangeMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListSwapMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.list.kopt.KOptListMoveSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner",
+ "allDeclaredFields": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.heuristic.selector.value.chained.SubChainSelectorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.localsearch.LocalSearchType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.localsearch.decider.acceptor.AcceptorType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.localsearch.decider.acceptor.LocalSearchAcceptorConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.localsearch.decider.forager.FinalistPodiumType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.localsearch.decider.forager.LocalSearchForagerConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.localsearch.decider.forager.LocalSearchPickEarlyType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.partitionedsearch.PartitionedSearchPhaseConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.phase.NoChangePhaseConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.phase.PhaseConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.phase.custom.CustomPhaseConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.solver.EnvironmentMode",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.solver.SolverConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.solver.monitoring.MonitoringConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.config.solver.monitoring.SolverMetric",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.solver.random.RandomType",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.solver.termination.TerminationCompositionStyle",
+ "allDeclaredFields": true
+ },
+ {
+ "name": "ai.timefold.solver.core.config.solver.termination.TerminationConfig",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbCustomPropertiesAdapter",
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbCustomPropertiesAdapter$JaxbAdaptedMap",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbCustomPropertiesAdapter$JaxbAdaptedMapEntry",
+ "allDeclaredFields": true,
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "jakarta.xml.bind.Binder"
+ },
+ {
+ "name": "jakarta.xml.bind.annotation.XmlAccessorType",
+ "queryAllDeclaredMethods": true
+ },
+ {
+ "name": "jakarta.xml.bind.annotation.XmlElement",
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "type",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "jakarta.xml.bind.annotation.XmlElements",
+ "queryAllDeclaredMethods": true
+ },
+ {
+ "name": "jakarta.xml.bind.annotation.XmlEnum",
+ "methods": [
+ {
+ "name": "value",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "jakarta.xml.bind.annotation.XmlRootElement",
+ "queryAllDeclaredMethods": true
+ },
+ {
+ "name": "jakarta.xml.bind.annotation.XmlSeeAlso",
+ "methods": [
+ {
+ "name": "value",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "jakarta.xml.bind.annotation.XmlTransient",
+ "queryAllDeclaredMethods": true
+ },
+ {
+ "name": "jakarta.xml.bind.annotation.XmlType",
+ "queryAllDeclaredMethods": true,
+ "methods": [
+ {
+ "name": "factoryClass",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter",
+ "methods": [
+ {
+ "name": "type",
+ "parameterTypes": []
+ },
+ {
+ "name": "value",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "org.glassfish.jaxb.core.v2.model.nav.ReflectionNavigator",
+ "methods": [
+ {
+ "name": "getInstance",
+ "parameterTypes": []
+ }
+ ]
+ },
+ {
+ "name": "org.glassfish.jaxb.runtime.v2.runtime.property.ArrayElementLeafProperty",
+ "queryAllPublicConstructors": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "org.glassfish.jaxb.runtime.v2.runtime.JAXBContextImpl",
+ "org.glassfish.jaxb.runtime.v2.model.runtime.RuntimeElementPropertyInfo"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "org.glassfish.jaxb.runtime.v2.runtime.property.ArrayElementNodeProperty",
+ "queryAllPublicConstructors": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "org.glassfish.jaxb.runtime.v2.runtime.JAXBContextImpl",
+ "org.glassfish.jaxb.runtime.v2.model.runtime.RuntimeElementPropertyInfo"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "org.glassfish.jaxb.runtime.v2.runtime.property.ArrayReferenceNodeProperty",
+ "queryAllPublicConstructors": true
+ },
+ {
+ "name": "org.glassfish.jaxb.runtime.v2.runtime.property.SingleElementLeafProperty",
+ "queryAllPublicConstructors": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "org.glassfish.jaxb.runtime.v2.runtime.JAXBContextImpl",
+ "org.glassfish.jaxb.runtime.v2.model.runtime.RuntimeElementPropertyInfo"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "org.glassfish.jaxb.runtime.v2.runtime.property.SingleElementNodeProperty",
+ "queryAllPublicConstructors": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "org.glassfish.jaxb.runtime.v2.runtime.JAXBContextImpl",
+ "org.glassfish.jaxb.runtime.v2.model.runtime.RuntimeElementPropertyInfo"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "org.glassfish.jaxb.runtime.v2.runtime.property.SingleMapNodeProperty",
+ "queryAllPublicConstructors": true
+ },
+ {
+ "name": "org.glassfish.jaxb.runtime.v2.runtime.property.SingleReferenceNodeProperty",
+ "queryAllPublicConstructors": true
+ },
+ {
+ "name": "org.glassfish.jersey.server.spring.SpringComponentProvider"
+ }
+]
\ No newline at end of file
diff --git a/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/resource-config.json b/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/resource-config.json
new file mode 100644
index 0000000000..d722763efd
--- /dev/null
+++ b/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/resource-config.json
@@ -0,0 +1,9 @@
+{
+ "resources": {
+ "includes": [
+ {
+ "pattern": "solver.xsd"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 10c855d5a8..4ab87107aa 100644
--- a/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1,2 +1,3 @@
-ai.timefold.solver.spring.boot.autoconfigure.TimefoldAutoConfiguration
-ai.timefold.solver.spring.boot.autoconfigure.TimefoldBenchmarkAutoConfiguration
\ No newline at end of file
+ai.timefold.solver.spring.boot.autoconfigure.TimefoldSolverAutoConfiguration
+ai.timefold.solver.spring.boot.autoconfigure.TimefoldBenchmarkAutoConfiguration
+ai.timefold.solver.spring.boot.autoconfigure.TimefoldSolverBeanFactory
\ No newline at end of file
diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAutoConfigurationTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfigurationTest.java
similarity index 94%
rename from spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAutoConfigurationTest.java
rename to spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfigurationTest.java
index 5dc7e00b8a..85a1bb1c73 100644
--- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAutoConfigurationTest.java
+++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfigurationTest.java
@@ -2,7 +2,9 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Duration;
import java.util.Collections;
@@ -68,7 +70,7 @@
import org.springframework.test.context.TestExecutionListeners;
@TestExecutionListeners
-class TimefoldAutoConfigurationTest {
+class TimefoldSolverAutoConfigurationTest {
private final ApplicationContextRunner contextRunner;
private final ApplicationContextRunner emptyContextRunner;
@@ -81,26 +83,32 @@ class TimefoldAutoConfigurationTest {
private final FilteredClassLoader testFilteredClassLoader;
private final FilteredClassLoader noGizmoFilteredClassLoader;
- public TimefoldAutoConfigurationTest() {
+ public TimefoldSolverAutoConfigurationTest() {
contextRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class))
+ .withConfiguration(
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class))
.withUserConfiguration(NormalSpringTestConfiguration.class);
emptyContextRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class))
+ .withConfiguration(
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class))
.withUserConfiguration(EmptySpringTestConfiguration.class);
benchmarkContextRunner = new ApplicationContextRunner()
.withConfiguration(
- AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBenchmarkAutoConfiguration.class))
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class,
+ TimefoldBenchmarkAutoConfiguration.class))
.withUserConfiguration(NormalSpringTestConfiguration.class);
gizmoContextRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class))
+ .withConfiguration(
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class))
.withUserConfiguration(GizmoSpringTestConfiguration.class);
chainedContextRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class))
+ .withConfiguration(
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class))
.withUserConfiguration(ChainedSpringTestConfiguration.class);
multimoduleRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class))
+ .withConfiguration(
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class))
.withUserConfiguration(MultiModuleSpringTestConfiguration.class);
allDefaultsFilteredClassLoader =
new FilteredClassLoader(FilteredClassLoader.PackageFilter.of("ai.timefold.solver.test"),
@@ -112,7 +120,8 @@ public TimefoldAutoConfigurationTest() {
FilteredClassLoader.ClassPathResourceFilter.of(
new ClassPathResource(TimefoldProperties.DEFAULT_SOLVER_CONFIG_URL)));
noUserConfigurationContextRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class));
+ .withConfiguration(
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class));
}
@Test
@@ -467,7 +476,7 @@ void benchmarkWithSpentLimit() {
problem.setEntityList(IntStream.range(1, 3)
.mapToObj(i -> new TestdataSpringEntity())
.collect(Collectors.toList()));
- benchmarkFactory.buildPlannerBenchmark(problem).benchmark();
+ assertThat(benchmarkFactory.buildPlannerBenchmark(problem).benchmark()).isNotEmptyDirectory();
});
}
@@ -485,7 +494,7 @@ void benchmark() {
problem.setEntityList(IntStream.range(1, 3)
.mapToObj(i -> new TestdataSpringEntity())
.collect(Collectors.toList()));
- benchmarkFactory.buildPlannerBenchmark(problem).benchmark();
+ assertThat(benchmarkFactory.buildPlannerBenchmark(problem).benchmark()).isNotEmptyDirectory();
});
}
@@ -505,7 +514,7 @@ void benchmarkWithXml() {
problem.setEntityList(IntStream.range(1, 3)
.mapToObj(i -> new TestdataSpringEntity())
.collect(Collectors.toList()));
- benchmarkFactory.buildPlannerBenchmark(problem).benchmark();
+ assertThat(benchmarkFactory.buildPlannerBenchmark(problem).benchmark()).isNotEmptyDirectory();
});
}
@@ -649,7 +658,7 @@ void multipleEasyScoreConstraints() {
.withPropertyValues("timefold.solver.termination.best-score-limit=0")
.run(context -> context.getBean("solver1")))
.cause().message().contains(
- "Multiple score classes classes", DummyChainedSpringEasyScore.class.getSimpleName(),
+ "Multiple score calculator classes", DummyChainedSpringEasyScore.class.getSimpleName(),
DummySpringEasyScore.class.getSimpleName(),
"that implements EasyScoreCalculator were found in the classpath.");
}
@@ -661,7 +670,7 @@ void multipleConstraintProviderConstraints() {
.withPropertyValues("timefold.solver.termination.best-score-limit=0")
.run(context -> context.getBean("solver1")))
.cause().message().contains(
- "Multiple score classes classes", TestdataChainedSpringConstraintProvider.class.getSimpleName(),
+ "Multiple score calculator classes", TestdataChainedSpringConstraintProvider.class.getSimpleName(),
TestdataSpringConstraintProvider.class.getSimpleName(),
"that implements ConstraintProvider were found in the classpath.");
}
@@ -673,7 +682,7 @@ void multipleIncrementalScoreConstraints() {
.withPropertyValues("timefold.solver.termination.best-score-limit=0")
.run(context -> context.getBean("solver1")))
.cause().message().contains(
- "Multiple score classes classes", DummyChainedSpringIncrementalScore.class.getSimpleName(),
+ "Multiple score calculator classes", DummyChainedSpringIncrementalScore.class.getSimpleName(),
DummySpringIncrementalScore.class.getSimpleName(),
"that implements IncrementalScoreCalculator were found in the classpath.");
}
@@ -686,7 +695,7 @@ void multipleEasyScoreConstraintsXml_property() {
"timefold.solver.solver.solver-config-xml=solverConfig.xml")
.run(context -> context.getBean("solver1")))
.cause().message().contains(
- "Multiple score classes classes",
+ "Multiple score calculator classes",
DummyChainedSpringEasyScore.class.getSimpleName(),
DummySpringEasyScore.class.getSimpleName(),
"that implements EasyScoreCalculator were found in the classpath");
@@ -700,7 +709,7 @@ void multipleConstraintProviderConstraintsXml_property() {
"timefold.solver.solver-config-xml=ai/timefold/solver/spring/boot/autoconfigure/normalSolverConfig.xml")
.run(context -> context.getBean("solver1")))
.cause().message().contains(
- "Multiple score classes classes", TestdataChainedSpringConstraintProvider.class.getSimpleName(),
+ "Multiple score calculator classes", TestdataChainedSpringConstraintProvider.class.getSimpleName(),
TestdataSpringConstraintProvider.class.getSimpleName(),
"that implements ConstraintProvider were found in the classpath.");
}
@@ -713,7 +722,7 @@ void multipleIncrementalScoreConstraintsXml_property() {
"timefold.solver.solver-config-xml=ai/timefold/solver/spring/boot/autoconfigure/normalSolverConfig.xml")
.run(context -> context.getBean("solver1")))
.cause().message().contains(
- "Multiple score classes classes", DummyChainedSpringIncrementalScore.class.getSimpleName(),
+ "Multiple score calculator classes", DummyChainedSpringIncrementalScore.class.getSimpleName(),
DummySpringIncrementalScore.class.getSimpleName(),
"that implements IncrementalScoreCalculator were found in the classpath.");
}
diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldMultipleSolverAutoConfigurationTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverMultipleSolverAutoConfigurationTest.java
similarity index 96%
rename from spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldMultipleSolverAutoConfigurationTest.java
rename to spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverMultipleSolverAutoConfigurationTest.java
index 91dc5e3aa5..60613d7691 100644
--- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldMultipleSolverAutoConfigurationTest.java
+++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverMultipleSolverAutoConfigurationTest.java
@@ -56,7 +56,7 @@
import org.springframework.test.context.TestExecutionListeners;
@TestExecutionListeners
-class TimefoldMultipleSolverAutoConfigurationTest {
+class TimefoldSolverMultipleSolverAutoConfigurationTest {
private final ApplicationContextRunner contextRunner;
private final ApplicationContextRunner emptyContextRunner;
@@ -67,32 +67,39 @@ class TimefoldMultipleSolverAutoConfigurationTest {
private final ApplicationContextRunner multimoduleRunner;
private final FilteredClassLoader allDefaultsFilteredClassLoader;
- public TimefoldMultipleSolverAutoConfigurationTest() {
+ public TimefoldSolverMultipleSolverAutoConfigurationTest() {
contextRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class))
+ .withConfiguration(
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class))
.withUserConfiguration(NormalSpringTestConfiguration.class);
emptyContextRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class))
+ .withConfiguration(
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class))
.withUserConfiguration(EmptySpringTestConfiguration.class);
benchmarkContextRunner = new ApplicationContextRunner()
.withConfiguration(
- AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBenchmarkAutoConfiguration.class))
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class,
+ TimefoldBenchmarkAutoConfiguration.class))
.withUserConfiguration(NormalSpringTestConfiguration.class);
gizmoContextRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class))
+ .withConfiguration(
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class))
.withUserConfiguration(GizmoSpringTestConfiguration.class);
chainedContextRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class))
+ .withConfiguration(
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class))
.withUserConfiguration(ChainedSpringTestConfiguration.class);
multimoduleRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class))
+ .withConfiguration(
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class))
.withUserConfiguration(MultiModuleSpringTestConfiguration.class);
allDefaultsFilteredClassLoader =
new FilteredClassLoader(FilteredClassLoader.PackageFilter.of("ai.timefold.solver.test"),
FilteredClassLoader.ClassPathResourceFilter
.of(new ClassPathResource(TimefoldProperties.DEFAULT_SOLVER_CONFIG_URL)));
noUserConfigurationContextRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class));
+ .withConfiguration(
+ AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class));
}
@Test
@@ -551,7 +558,7 @@ void multipleEasyScoreConstraints() {
.run(context -> context.getBean("solver1")))
.cause().message().contains(
"Some solver configs", "solver2", "solver1",
- "don't specify a EasyScoreCalculator score class, yet there are multiple available",
+ "don't specify a EasyScoreCalculator score calculator class, yet there are multiple available",
DummyChainedSpringEasyScore.class.getSimpleName(),
DummySpringEasyScore.class.getSimpleName(),
"on the classpath.");
@@ -566,7 +573,7 @@ void multipleConstraintProviderConstraints() {
.run(context -> context.getBean("solver1")))
.cause().message().contains(
"Some solver configs", "solver2", "solver1",
- "don't specify a ConstraintProvider score class, yet there are multiple available",
+ "don't specify a ConstraintProvider score calculator class, yet there are multiple available",
TestdataChainedSpringConstraintProvider.class.getSimpleName(),
TestdataSpringConstraintProvider.class.getSimpleName(),
"on the classpath.");
@@ -581,7 +588,7 @@ void multipleIncrementalScoreConstraints() {
.run(context -> context.getBean("solver1")))
.cause().message().contains(
"Some solver configs", "solver2", "solver1",
- "don't specify a IncrementalScoreCalculator score class, yet there are multiple available",
+ "don't specify a IncrementalScoreCalculator score calculator class, yet there are multiple available",
DummyChainedSpringIncrementalScore.class.getSimpleName(),
DummySpringIncrementalScore.class.getSimpleName(),
"on the classpath.");
@@ -598,7 +605,7 @@ void multipleEasyScoreConstraintsXml_property() {
.run(context -> context.getBean("solver1")))
.cause().message().contains(
"Some solver configs", "solver2", "solver1",
- "don't specify a EasyScoreCalculator score class, yet there are multiple available",
+ "don't specify a EasyScoreCalculator score calculator class, yet there are multiple available",
DummyChainedSpringEasyScore.class.getSimpleName(),
DummySpringEasyScore.class.getSimpleName(),
"on the classpath.");
@@ -615,7 +622,7 @@ void multipleConstraintProviderConstraintsXml_property() {
.run(context -> context.getBean("solver1")))
.cause().message().contains(
"Some solver configs", "solver2", "solver1",
- "don't specify a ConstraintProvider score class, yet there are multiple available",
+ "don't specify a ConstraintProvider score calculator class, yet there are multiple available",
TestdataChainedSpringConstraintProvider.class.getSimpleName(),
TestdataSpringConstraintProvider.class.getSimpleName(),
"on the classpath.");
@@ -632,7 +639,7 @@ void multipleIncrementalScoreConstraintsXml_property() {
.run(context -> context.getBean("solver1")))
.cause().message().contains(
"Some solver configs", "solver2", "solver1",
- "don't specify a IncrementalScoreCalculator score class, yet there are multiple available",
+ "don't specify a IncrementalScoreCalculator score calculator class, yet there are multiple available",
DummyChainedSpringIncrementalScore.class.getSimpleName(),
DummySpringIncrementalScore.class.getSimpleName(),
"on the classpath.");
diff --git a/spring-integration/spring-boot-integration-test/pom.xml b/spring-integration/spring-boot-integration-test/pom.xml
new file mode 100644
index 0000000000..a44ecdb71a
--- /dev/null
+++ b/spring-integration/spring-boot-integration-test/pom.xml
@@ -0,0 +1,148 @@
+
+
+ 4.0.0
+
+ ai.timefold.solver
+ timefold-solver-spring-integration
+ 999-SNAPSHOT
+
+
+ spring-boot-integration-test
+
+
+ ai.timefold.solver.spring.boot
+
+ **/*
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ pom
+ import
+ ${version.org.springframework.boot}
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ ai.timefold.solver
+ timefold-solver-spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework
+ spring-webflux
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+ true
+
+
+
+
+
+
+
+ native
+
+
+ native
+ true
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ timefold-spring-boot-integration-test
+
+ paketobuildpacks/builder:tiny
+
+ true
+
+
+
+
+
+ process-aot
+
+ process-aot
+ process-test-aot
+
+
+
+
+
+ org.graalvm.buildtools
+ native-maven-plugin
+ true
+
+
+ build-native
+
+ compile-no-fork
+ test
+
+ package
+
+ app.native
+
+
+
+ add-reachability-metadata
+
+ add-reachability-metadata
+
+
+
+
+ app.native
+
+
+ -Ob
+ --no-fallback
+
+
+ true
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSolverController.java b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSolverController.java
new file mode 100644
index 0000000000..95d80ee0d5
--- /dev/null
+++ b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSolverController.java
@@ -0,0 +1,25 @@
+package ai.timefold.solver.spring.boot.it;
+
+import ai.timefold.solver.core.api.solver.SolverFactory;
+import ai.timefold.solver.spring.boot.it.domain.IntegrationTestSolution;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/integration-test")
+public class TimefoldSolverController {
+ private final SolverFactory solverFactory;
+
+ public TimefoldSolverController(SolverFactory solverFactory) {
+ this.solverFactory = solverFactory;
+ }
+
+ @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+ public IntegrationTestSolution solve(@RequestBody IntegrationTestSolution problem) {
+ return solverFactory.buildSolver().solve(problem);
+ }
+}
diff --git a/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSolverSpringBootApp.java b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSolverSpringBootApp.java
new file mode 100644
index 0000000000..bc26a7a1eb
--- /dev/null
+++ b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSolverSpringBootApp.java
@@ -0,0 +1,12 @@
+package ai.timefold.solver.spring.boot.it;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class TimefoldSolverSpringBootApp {
+
+ public static void main(String[] args) {
+ SpringApplication.run(TimefoldSolverSpringBootApp.class, args);
+ }
+}
diff --git a/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/domain/IntegrationTestEntity.java b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/domain/IntegrationTestEntity.java
new file mode 100644
index 0000000000..564954e599
--- /dev/null
+++ b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/domain/IntegrationTestEntity.java
@@ -0,0 +1,37 @@
+package ai.timefold.solver.spring.boot.it.domain;
+
+import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
+import ai.timefold.solver.core.api.domain.lookup.PlanningId;
+import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
+
+@PlanningEntity
+public class IntegrationTestEntity {
+ @PlanningId
+ private String id;
+
+ @PlanningVariable
+ private IntegrationTestValue value;
+
+ public IntegrationTestEntity() {
+ }
+
+ public IntegrationTestEntity(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public IntegrationTestValue getValue() {
+ return value;
+ }
+
+ public void setValue(IntegrationTestValue value) {
+ this.value = value;
+ }
+}
diff --git a/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/domain/IntegrationTestSolution.java b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/domain/IntegrationTestSolution.java
new file mode 100644
index 0000000000..e860d3a1ef
--- /dev/null
+++ b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/domain/IntegrationTestSolution.java
@@ -0,0 +1,55 @@
+package ai.timefold.solver.spring.boot.it.domain;
+
+import java.util.List;
+
+import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty;
+import ai.timefold.solver.core.api.domain.solution.PlanningScore;
+import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
+import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
+import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
+import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore;
+
+@PlanningSolution
+public class IntegrationTestSolution {
+ @PlanningEntityCollectionProperty
+ private List entityList;
+
+ @ValueRangeProvider
+ @ProblemFactCollectionProperty
+ private List valueList;
+
+ @PlanningScore
+ private SimpleScore score;
+
+ public IntegrationTestSolution() {
+ }
+
+ public IntegrationTestSolution(List entityList, List valueList) {
+ this.entityList = entityList;
+ this.valueList = valueList;
+ }
+
+ public List getEntityList() {
+ return entityList;
+ }
+
+ public void setEntityList(List entityList) {
+ this.entityList = entityList;
+ }
+
+ public List getValueList() {
+ return valueList;
+ }
+
+ public void setValueList(List valueList) {
+ this.valueList = valueList;
+ }
+
+ public SimpleScore getScore() {
+ return score;
+ }
+
+ public void setScore(SimpleScore score) {
+ this.score = score;
+ }
+}
diff --git a/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/domain/IntegrationTestValue.java b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/domain/IntegrationTestValue.java
new file mode 100644
index 0000000000..026e63ab55
--- /dev/null
+++ b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/domain/IntegrationTestValue.java
@@ -0,0 +1,4 @@
+package ai.timefold.solver.spring.boot.it.domain;
+
+public record IntegrationTestValue(String id) {
+}
diff --git a/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/solver/IntegrationTestConstraintProvider.java b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/solver/IntegrationTestConstraintProvider.java
new file mode 100644
index 0000000000..120b7b9445
--- /dev/null
+++ b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/solver/IntegrationTestConstraintProvider.java
@@ -0,0 +1,19 @@
+package ai.timefold.solver.spring.boot.it.solver;
+
+import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore;
+import ai.timefold.solver.core.api.score.stream.Constraint;
+import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
+import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
+import ai.timefold.solver.spring.boot.it.domain.IntegrationTestEntity;
+
+public class IntegrationTestConstraintProvider implements ConstraintProvider {
+ @Override
+ public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
+ return new Constraint[] {
+ constraintFactory.forEach(IntegrationTestEntity.class)
+ .filter(entity -> !entity.getId().equals(entity.getValue().id()))
+ .penalize(SimpleScore.ONE)
+ .asConstraint("Entity id do not match value id")
+ };
+ }
+}
diff --git a/spring-integration/spring-boot-integration-test/src/main/resources/application.properties b/spring-integration/spring-boot-integration-test/src/main/resources/application.properties
new file mode 100644
index 0000000000..00ccc41834
--- /dev/null
+++ b/spring-integration/spring-boot-integration-test/src/main/resources/application.properties
@@ -0,0 +1 @@
+timefold.solver.termination.best-score-limit=0
\ No newline at end of file
diff --git a/spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/TimefoldSolverTestResourceIntegrationTest.java b/spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/TimefoldSolverTestResourceIntegrationTest.java
new file mode 100644
index 0000000000..50460b95f6
--- /dev/null
+++ b/spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/TimefoldSolverTestResourceIntegrationTest.java
@@ -0,0 +1,72 @@
+package ai.timefold.solver.spring.boot.it;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+
+import ai.timefold.solver.core.config.solver.SolverConfig;
+import ai.timefold.solver.core.impl.io.jaxb.SolverConfigIO;
+import ai.timefold.solver.spring.boot.it.domain.IntegrationTestEntity;
+import ai.timefold.solver.spring.boot.it.domain.IntegrationTestSolution;
+import ai.timefold.solver.spring.boot.it.domain.IntegrationTestValue;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.core.io.Resource;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class TimefoldSolverTestResourceIntegrationTest {
+
+ @LocalServerPort
+ String port;
+
+ @Value("classpath:solver-full.xml")
+ Resource exampleSolverConfigXml;
+
+ @Test
+ void testSolve() {
+ WebTestClient client = WebTestClient.bindToServer()
+ .baseUrl("http://localhost:" + port + "/integration-test")
+ .build();
+
+ IntegrationTestSolution problem = new IntegrationTestSolution(
+ List.of(new IntegrationTestEntity("0"),
+ new IntegrationTestEntity("1"),
+ new IntegrationTestEntity("2")),
+ List.of(new IntegrationTestValue("0"),
+ new IntegrationTestValue("1"),
+ new IntegrationTestValue("2")));
+ client.post()
+ .bodyValue(problem)
+ .exchange()
+ .expectBody()
+ .jsonPath("score").isEqualTo("0")
+ .jsonPath("entityList").isArray()
+ .jsonPath("valueList").isArray()
+ .jsonPath("entityList[0].id").isEqualTo("0")
+ .jsonPath("entityList[0].value.id").isEqualTo("0")
+ .jsonPath("entityList[1].id").isEqualTo("1")
+ .jsonPath("entityList[1].value.id").isEqualTo("1")
+ .jsonPath("entityList[2].id").isEqualTo("2")
+ .jsonPath("entityList[2].value.id").isEqualTo("2");
+
+ }
+
+ @Test
+ void testSolverXmlParsing() throws IOException {
+ // Test to verify parsing a complex SolverConfig will work in native image.
+ // XML file was generated by taking the XSD file available at
+ // https://timefold.ai/xsd/solver and generating an XML file from it using
+ // the "Tools > XML Actions > Generate XML Document from XML Schema..." action in IDEA
+ SolverConfigIO solverConfigIO = new SolverConfigIO();
+ SolverConfig solverConfig = solverConfigIO.read(new InputStreamReader(exampleSolverConfigXml.getInputStream()));
+ assertThat(solverConfig).isNotNull();
+ assertThat(solverConfig.getSolutionClass()).isEqualTo(Object.class);
+ assertThat(solverConfig.getPhaseConfigList()).isNotEmpty();
+ }
+}
diff --git a/spring-integration/spring-boot-integration-test/src/test/resources/META-INF/native-image/resource-config.json b/spring-integration/spring-boot-integration-test/src/test/resources/META-INF/native-image/resource-config.json
new file mode 100644
index 0000000000..d0bd48b897
--- /dev/null
+++ b/spring-integration/spring-boot-integration-test/src/test/resources/META-INF/native-image/resource-config.json
@@ -0,0 +1,9 @@
+{
+ "resources": {
+ "includes": [
+ {
+ "pattern": "solver-full.xml"
+ }
+ ]
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..54d9e0b4d1
--- /dev/null
+++ b/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml
@@ -0,0 +1,1151 @@
+
+ TRACKED_FULL_ASSERT
+ false
+ WELL1024A
+ 10
+ java.lang.Object
+ 1
+ 3
+ java.lang.Object
+
+
+ MEMORY_USE
+
+ java.lang.Object
+
+ java.lang.Object
+ GIZMO
+
+ java.lang.Object
+
+
+
+
+ java.lang.Object
+
+
+
+
+ DROOLS
+ java.lang.Object
+
+
+
+
+ string
+
+
+
+ java.lang.Object
+ AND
+ PT8H
+ 10
+ 10
+ 10
+ 10
+ 10
+ string
+ 10
+ 10
+ 10
+ 10
+ 10
+ string
+ string
+ true
+ 3
+ 3
+ 10
+
+
+
+
+
+
+ java.lang.Object
+ AND
+ PT8H
+ 10
+ 10
+ 10
+ 10
+ 10
+ string
+ 10
+ 10
+ 10
+ 10
+ 10
+ string
+ string
+ false
+ 3
+ 3
+ 10
+
+
+
+ STRONGEST_FIT
+ NONE
+ DECREASING_STRENGTH
+
+
+
+ java.lang.Object
+ SOLVER
+ SHUFFLED
+
+
+
+
+ java.lang.Object
+ STEP
+ SHUFFLED
+
+ java.lang.Object
+ NONE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ STEP
+ PROBABILISTIC
+
+ java.lang.Object
+ INCREASING_STRENGTH
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+ java.lang.Object
+ BETA_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ NONE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+ PHASE
+ SORTED
+ java.lang.Object
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+ 1.051732E7
+
+
+
+ JUST_IN_TIME
+ SHUFFLED
+ java.lang.Object
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+ 1.051732E7
+
+ java.lang.Object
+ SOLVER
+ PROBABILISTIC
+
+
+
+
+ java.lang.Object
+ SOLVER
+ SHUFFLED
+
+ java.lang.Object
+ DECREASING_STRENGTH_IF_AVAILABLE
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ JUST_IN_TIME
+ SORTED
+
+ java.lang.Object
+ NONE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+ java.lang.Object
+ LINEAR_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ DECREASING_DIFFICULTY_IF_AVAILABLE
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+ java.lang.Object
+ PHASE
+ SHUFFLED
+
+
+ java.lang.Object
+ PHASE
+ INHERIT
+
+ java.lang.Object
+ DECREASING_DIFFICULTY
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ BLOCK_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ INCREASING_STRENGTH_IF_AVAILABLE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+ PHASE
+ RANDOM
+ java.lang.Object
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+ 1.051732E7
+ 3
+ 3
+
+ java.lang.Object
+ SOLVER
+ SORTED
+
+
+ java.lang.Object
+ SOLVER
+ SHUFFLED
+
+ java.lang.Object
+ DECREASING_DIFFICULTY
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ BLOCK_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ DECREASING_STRENGTH
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+ java.lang.Object
+ JUST_IN_TIME
+ RANDOM
+
+
+ java.lang.Object
+ SOLVER
+ PROBABILISTIC
+
+ java.lang.Object
+ NONE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ PARABOLIC_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ NONE
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+ STEP
+ PROBABILISTIC
+ java.lang.Object
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+ 1.051732E7
+
+ java.lang.Object
+ SOLVER
+ PROBABILISTIC
+
+
+ java.lang.Object
+ JUST_IN_TIME
+ INHERIT
+
+ java.lang.Object
+ DECREASING_DIFFICULTY
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ LINEAR_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ INCREASING_STRENGTH
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+ java.lang.Object
+ STEP
+ INHERIT
+
+
+
+
+ java.lang.Object
+ JUST_IN_TIME
+ SHUFFLED
+
+ java.lang.Object
+ DECREASING_STRENGTH
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ PHASE
+ SHUFFLED
+
+ java.lang.Object
+ NONE
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+ java.lang.Object
+ BLOCK_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ NONE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+ java.lang.Object
+ JUST_IN_TIME
+ SHUFFLED
+
+
+ java.lang.Object
+ STEP
+ RANDOM
+
+ java.lang.Object
+ DECREASING_DIFFICULTY
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ BLOCK_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ DECREASING_STRENGTH
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+ java.lang.Object
+ JUST_IN_TIME
+ INHERIT
+
+ java.lang.Object
+ DECREASING_DIFFICULTY
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+ java.lang.Object
+ PHASE
+ SHUFFLED
+
+ java.lang.Object
+ DECREASING_STRENGTH_IF_AVAILABLE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ SOLVER
+ ORIGINAL
+
+ java.lang.Object
+ NONE
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+ java.lang.Object
+ BLOCK_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+
+
+
+ JUST_IN_TIME
+ ORIGINAL
+ java.lang.Object
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+ 1.051732E7
+
+ java.lang.Object
+ PHASE
+ RANDOM
+
+
+ java.lang.Object
+ JUST_IN_TIME
+ INHERIT
+
+ java.lang.Object
+ DECREASING_DIFFICULTY_IF_AVAILABLE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ LINEAR_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ DECREASING_STRENGTH_IF_AVAILABLE
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+ java.lang.Object
+ PHASE
+ SORTED
+
+
+ java.lang.Object
+ STEP
+ RANDOM
+
+ java.lang.Object
+ NONE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ BETA_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ INCREASING_STRENGTH
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+ PHASE
+ PROBABILISTIC
+ java.lang.Object
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+ 1.051732E7
+ java.lang.Object
+
+
+
+
+
+
+ STEP
+ SORTED
+ java.lang.Object
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+ 1.051732E7
+ java.lang.Object
+
+
+
+
+
+
+ SOLVER
+ INHERIT
+ java.lang.Object
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+ 1.051732E7
+ ALL
+ java.lang.Object
+
+
+ java.lang.Object
+ PHASE
+ SHUFFLED
+
+
+
+
+ java.lang.Object
+ PHASE
+ RANDOM
+
+ java.lang.Object
+ INCREASING_STRENGTH
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ PHASE
+ ORIGINAL
+
+ java.lang.Object
+ INCREASING_STRENGTH_IF_AVAILABLE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+ java.lang.Object
+ BLOCK_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ DECREASING_DIFFICULTY
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+ 3
+ 3
+
+
+ java.lang.Object
+ STEP
+ SORTED
+
+
+ java.lang.Object
+ PHASE
+ ORIGINAL
+
+ java.lang.Object
+ NONE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ BETA_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ DECREASING_STRENGTH
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+ STEP
+ PROBABILISTIC
+ java.lang.Object
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+ 1.051732E7
+ ALL
+ java.lang.Object
+
+
+ java.lang.Object
+ PHASE
+ INHERIT
+
+
+
+
+ java.lang.Object
+ JUST_IN_TIME
+ INHERIT
+
+ java.lang.Object
+ INCREASING_STRENGTH
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ JUST_IN_TIME
+ SHUFFLED
+
+ java.lang.Object
+ DECREASING_STRENGTH_IF_AVAILABLE
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+ java.lang.Object
+ LINEAR_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ DECREASING_DIFFICULTY_IF_AVAILABLE
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+ 3
+ 3
+
+
+
+ java.lang.Object
+ PHASE
+ PROBABILISTIC
+
+
+
+
+ java.lang.Object
+ STEP
+ INHERIT
+
+ java.lang.Object
+ DECREASING_STRENGTH
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ SOLVER
+ RANDOM
+
+ java.lang.Object
+ INCREASING_STRENGTH_IF_AVAILABLE
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+ java.lang.Object
+ BETA_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ DECREASING_DIFFICULTY
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+ 3
+ 3
+
+
+ string
+
+
+
+ STEP
+ SHUFFLED
+ java.lang.Object
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+ 1.051732E7
+ java.lang.Object
+
+
+ java.lang.Object
+ PHASE
+ SORTED
+
+
+ java.lang.Object
+ JUST_IN_TIME
+ PROBABILISTIC
+
+ java.lang.Object
+ NONE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ LINEAR_DISTRIBUTION
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ 3
+ 3
+ 1.051732E7
+ 1.051732E7
+
+ java.lang.Object
+ NONE
+ java.lang.Object
+ java.lang.Object
+ DESCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+ 3
+ 3
+
+
+ java.lang.Object
+ PHASE
+ RANDOM
+
+
+ java.lang.Object
+ STEP
+ SORTED
+
+ java.lang.Object
+ DECREASING_DIFFICULTY
+ java.lang.Object
+ java.lang.Object
+ ASCENDING
+ java.lang.Object
+ java.lang.Object
+ 10
+
+
+
+
+ 3
+ 3
+
+
+ java.lang.Object
+ BLOCK_DISTRIBUTION
+ 3
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file