From c9e678fe8e42e3c72e9b1c01ca82c2d9bbddfc4a Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 1 Feb 2024 14:47:07 -0500 Subject: [PATCH 01/12] feat: Spring boot native image support Spring boot does not seem to have a way to record POJOs to use them in generated files. I written a basic one that assumes all fields on POJOs have public setters and can serialize primitives, builtins, and collection types. --- spring-integration/pom.xml | 1 + .../spring-boot-autoconfigure/pom.xml | 4 + .../TimefoldAotContribution.java | 429 ++++++++++++++++++ .../TimefoldAutoConfiguration.java | 181 +------- .../autoconfigure/TimefoldBeanFactory.java | 204 +++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../TimefoldAutoConfigurationTest.java | 15 +- ...ldMultipleSolverAutoConfigurationTest.java | 15 +- .../spring-boot-integration-test/pom.xml | 148 ++++++ .../spring/boot/it/TimefoldController.java | 25 + .../spring/boot/it/TimefoldSpringBootApp.java | 12 + .../boot/it/domain/IntegrationTestEntity.java | 37 ++ .../it/domain/IntegrationTestSolution.java | 55 +++ .../boot/it/domain/IntegrationTestValue.java | 4 + .../IntegrationTestConstraintProvider.java | 19 + .../src/main/resources/application.properties | 1 + .../TimefoldTestResourceIntegrationTest.java | 48 ++ 17 files changed, 1027 insertions(+), 174 deletions(-) create mode 100644 spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java create mode 100644 spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldBeanFactory.java create mode 100644 spring-integration/spring-boot-integration-test/pom.xml create mode 100644 spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldController.java create mode 100644 spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSpringBootApp.java create mode 100644 spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/domain/IntegrationTestEntity.java create mode 100644 spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/domain/IntegrationTestSolution.java create mode 100644 spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/domain/IntegrationTestValue.java create mode 100644 spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/solver/IntegrationTestConstraintProvider.java create mode 100644 spring-integration/spring-boot-integration-test/src/main/resources/application.properties create mode 100644 spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/TimefoldTestResourceIntegrationTest.java 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..af503ff839 100644 --- a/spring-integration/spring-boot-autoconfigure/pom.xml +++ b/spring-integration/spring-boot-autoconfigure/pom.xml @@ -62,6 +62,10 @@ org.springframework.boot spring-boot-autoconfigure + + org.apache.commons + commons-text + org.springframework.boot diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java new file mode 100644 index 0000000000..03ae5b20cc --- /dev/null +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java @@ -0,0 +1,429 @@ +package ai.timefold.solver.spring.boot.autoconfigure; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.lang.model.element.Modifier; + +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.spring.boot.autoconfigure.config.SolverManagerProperties; +import ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties; + +import org.apache.commons.text.StringEscapeUtils; +import org.springframework.aot.generate.DefaultMethodReference; +import org.springframework.aot.generate.GeneratedClass; +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; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.env.Environment; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; + +public class TimefoldAotContribution implements BeanFactoryInitializationAotContribution { + private static final String DEFAULT_SOLVER_CONFIG_NAME = "getSolverConfig"; + + /** + * Name of the field that stores generated objects. + * Complex pojo's should consult this map before creating + * a new object to allow for cyclic references + * (i.e., a = new ArrayList(); a.add(a);). + */ + private static final String COMPLEX_POJO_MAP_FIELD_NAME = "$pojoMap"; + + /** + * Map of SolverConfigs that were recorded during the build. + */ + private final Map solverConfigMap; + + public TimefoldAotContribution(Map solverConfigMap) { + this.solverConfigMap = solverConfigMap; + } + + /** + * Register a type for reflection, allowing introspection + * of its members at runtime in a native build. + */ + private 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); + } + + // The code below uses CodeBlock.Builder to generate the Java file + // that stores the SolverConfig. + // CodeBlock.Builder.add supports different kinds of formatting args. + // The ones we use are: + // - $L: Format as is (i.e. literal replacement). + // - $S: Format as a Java String, doing the necessary escapes + // and surrounding it by double quotes. + // - $T: Format as a fully qualified type, which allows you to use + // classes without importing them. + + /** + * Serializes a Pojo to code that uses its no-args constructor + * and setters to create the object. + * + * @param pojo The object to be serialized. + * @param initializerCode The code block builder of the initializer + * @param complexPojoToIdentifier A map that stores objects already recorded + * @return A string that can be used in a {@link CodeBlock.Builder} to access the object + */ + public static String pojoToCode(Object pojo, + CodeBlock.Builder initializerCode, + Map complexPojoToIdentifier) { + // First, check for primitives + if (pojo == null) { + return "null"; + } + if (pojo instanceof Boolean value) { + return value.toString(); + } + if (pojo instanceof Byte value) { + return value.toString(); + } + if (pojo instanceof Character value) { + return "\\u" + Integer.toHexString(value | 0x10000).substring(1); + } + if (pojo instanceof Short value) { + return value.toString(); + } + if (pojo instanceof Integer value) { + return value.toString(); + } + if (pojo instanceof Long value) { + // Add long suffix to number string + return value + "L"; + } + if (pojo instanceof Float value) { + // Add float suffix to number string + return value + "f"; + } + if (pojo instanceof Double value) { + // Add double suffix to number string + return value + "d"; + } + + // Check for builtin classes + if (pojo instanceof String value) { + return "\"" + StringEscapeUtils.escapeJava(value) + "\""; + } + if (pojo instanceof Class value) { + return value.getName() + ".class"; + } + if (pojo instanceof ClassLoader) { + // We don't support serializing ClassLoaders, so replace it + // with the context class loader + return "Thread.currentThread().getContextClassLoader()"; + } + if (pojo.getClass().isEnum()) { + // Use field access to read the enum + Class enumClass = pojo.getClass(); + Enum pojoEnum = (Enum) pojo; + return enumClass.getName() + "." + pojoEnum.name(); + } + return complexPojoToCode(pojo, initializerCode, complexPojoToIdentifier); + } + + /** + * Return a string that can be used in a {@link CodeBlock.Builder} to access a complex object + * + * @param pojo The object to be accessed + * @param complexPojoToIdentifier A Map from complex POJOs to their key in the map. + * @return A string that can be used in a {@link CodeBlock.Builder} to access the object. + */ + private static String getComplexPojo(Object pojo, Map complexPojoToIdentifier) { + return "((" + pojo.getClass().getName() + ") " + COMPLEX_POJO_MAP_FIELD_NAME + ".get(\"" + + complexPojoToIdentifier.get(pojo) + "\"))"; + } + + /** + * Serializes collections and complex POJOs to code + */ + private static String complexPojoToCode(Object pojo, CodeBlock.Builder initializerCode, + Map complexPojoToIdentifier) { + // If we already serialized the object, we should just return + // the code string + if (complexPojoToIdentifier.containsKey(pojo)) { + return getComplexPojo(pojo, complexPojoToIdentifier); + } + // Object is not serialized yet + // Create a new variable to store its value when setting its fields + String newIdentifier = "$obj" + complexPojoToIdentifier.size(); + complexPojoToIdentifier.put(pojo, newIdentifier); + initializerCode.add("\n$T $L;", pojo.getClass(), newIdentifier); + + // First, check if it is a collection type + if (pojo.getClass().isArray()) { + return arrayToCode(newIdentifier, pojo, initializerCode, complexPojoToIdentifier); + } + if (pojo instanceof List value) { + return listToCode(newIdentifier, value, initializerCode, complexPojoToIdentifier); + } + if (pojo instanceof Set value) { + return setToCode(newIdentifier, value, initializerCode, complexPojoToIdentifier); + } + if (pojo instanceof Map value) { + return mapToCode(newIdentifier, value, initializerCode, complexPojoToIdentifier); + } + + // Not a collection type, so serialize by creating a new instance and settings its fields + initializerCode.add("\n$L = new $T();", newIdentifier, pojo.getClass()); + initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + setComplexPojoFields(pojo.getClass(), newIdentifier, pojo, initializerCode, complexPojoToIdentifier); + return getComplexPojo(pojo, complexPojoToIdentifier); + } + + private static String arrayToCode(String newIdentifier, Object array, CodeBlock.Builder initializerCode, + Map complexPojoToIdentifier) { + // Get the length of the array + int length = Array.getLength(array); + + // Create a new array from the component type with the given length + initializerCode.add("\n$L = new $T[$L];", newIdentifier, array.getClass().getComponentType(), Integer.toString(length)); + initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + for (int i = 0; i < length; i++) { + // Set the elements of the array + initializerCode.add("\n$L[$L] = $L;", + newIdentifier, + Integer.toString(i), + pojoToCode(Array.get(array, i), initializerCode, complexPojoToIdentifier)); + } + return getComplexPojo(array, complexPojoToIdentifier); + } + + private static String listToCode(String newIdentifier, List list, CodeBlock.Builder initializerCode, + Map complexPojoToIdentifier) { + // Create an ArrayList + initializerCode.add("\n$L = new $T($L);", newIdentifier, ArrayList.class, Integer.toString(list.size())); + initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + for (Object item : list) { + // Add each item of the list to the ArrayList + initializerCode.add("\n$L.add($L);", + newIdentifier, + pojoToCode(item, initializerCode, complexPojoToIdentifier)); + } + return getComplexPojo(list, complexPojoToIdentifier); + } + + private static String setToCode(String newIdentifier, Set set, CodeBlock.Builder initializerCode, + Map complexPojoToIdentifier) { + // Create a new HashSet + initializerCode.add("\n$L = new $T($L);", newIdentifier, HashSet.class, Integer.toString(set.size())); + initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + for (Object item : set) { + // Add each item of the set to the HashSet + initializerCode.add("\n$L.add($L);", + newIdentifier, + pojoToCode(item, initializerCode, complexPojoToIdentifier)); + } + return getComplexPojo(set, complexPojoToIdentifier); + } + + private static String mapToCode(String newIdentifier, Map map, CodeBlock.Builder initializerCode, + Map complexPojoToIdentifier) { + // Create a HashMap + initializerCode.add("\n$L = new $T($L);", newIdentifier, HashMap.class, Integer.toString(map.size())); + initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + for (Map.Entry entry : map.entrySet()) { + // Put each entry of the map into the HashMap + initializerCode.add("\n$L.put($L,$L);", + newIdentifier, + pojoToCode(entry.getKey(), initializerCode, complexPojoToIdentifier), + pojoToCode(entry.getValue(), initializerCode, complexPojoToIdentifier)); + } + return getComplexPojo(map, complexPojoToIdentifier); + } + + /** + * Sets the fields of pojo declared in pojoClass and all its superclasses. + * + * @param pojoClass A class assignable to pojo containing some of its fields. + * @param identifier The name of the variable storing the serialized pojo. + * @param pojo The object being serialized. + * @param initializerCode The {@link CodeBlock.Builder} to use to generate code in the initializer. + * @param complexPojoToIdentifier A map from complex POJOs to their variable name. + */ + private static void setComplexPojoFields(Class pojoClass, String identifier, Object pojo, + CodeBlock.Builder initializerCode, Map complexPojoToIdentifier) { + if (pojoClass == Object.class) { + // We are the top-level, no more fields to set + return; + } + for (Field field : pojo.getClass().getDeclaredFields()) { + if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) { + // We do not want to write static fields + continue; + } + // Set the field accessible so we can read its value + field.setAccessible(true); + try { + // Convert the field value to code, and call the setter + // corresponding to the field with the serialized field value. + initializerCode.add("\n$L.set$L$L($L);", identifier, + Character.toUpperCase(field.getName().charAt(0)), + field.getName().substring(1), + pojoToCode(field.get(pojo), initializerCode, complexPojoToIdentifier)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + setComplexPojoFields(pojoClass.getSuperclass(), identifier, pojo, initializerCode, complexPojoToIdentifier); + } + + @Override + public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { + var reflectionHints = generationContext.getRuntimeHints().reflection(); + + // Register all classes reachable from the SolverConfig for reflection + // (so we can read their metadata) + Set> classSet = new HashSet<>(); + for (SolverConfig solverConfig : solverConfigMap.values()) { + solverConfig.visitReferencedClasses(clazz -> { + if (clazz != null) { + classSet.add(clazz); + } + }); + } + + for (Class clazz : classSet) { + registerType(reflectionHints, clazz); + } + + // Create a generated class to hold all the solver configs + GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("timefold-aot", + builder -> { + builder.addField(Map.class, "solverConfigMap", Modifier.STATIC); + builder.addField(Map.class, COMPLEX_POJO_MAP_FIELD_NAME, Modifier.STATIC); + + // Handwrite the SolverConfig map in the initializer + CodeBlock.Builder staticInitializer = CodeBlock.builder(); + Map complexPojoToIdentifier = new IdentityHashMap<>(); + staticInitializer.add("$L = new $T();", COMPLEX_POJO_MAP_FIELD_NAME, HashMap.class); + staticInitializer.add("\nsolverConfigMap = $L;", + complexPojoToCode(solverConfigMap, staticInitializer, complexPojoToIdentifier)); + builder.addStaticBlock(staticInitializer.build()); + + // getSolverConfig fetches the SolverConfig with the given name from the map + CodeBlock.Builder getSolverConfigMethod = CodeBlock.builder(); + getSolverConfigMethod.add("return ($T) solverConfigMap.get(name);", SolverConfig.class); + builder.addMethod(MethodSpec.methodBuilder("getSolverConfig") + .addModifiers(Modifier.PUBLIC) + .addModifiers(Modifier.STATIC) + .addParameter(String.class, "name") + .returns(SolverConfig.class) + .addCode(getSolverConfigMethod.build()) + .build()); + + // Returns the key set of the solver config map + CodeBlock.Builder getSolverConfigNamesMethod = CodeBlock.builder(); + getSolverConfigNamesMethod.add("return new $T(solverConfigMap.keySet());", ArrayList.class); + builder.addMethod(MethodSpec.methodBuilder("getSolverConfigNames") + .addModifiers(Modifier.PUBLIC) + .addModifiers(Modifier.STATIC) + .returns(List.class) + .addCode(getSolverConfigNamesMethod.build()) + .build()); + + // Registers the SolverConfig(s) as beans that can be injected + CodeBlock.Builder registerSolverConfigsMethod = CodeBlock.builder(); + // Get the timefold properties from the environment + registerSolverConfigsMethod.add("$T timefoldPropertiesResult = " + + "$T.get(environment).bind(\"timefold\", $T.class);", BindResult.class, Binder.class, + TimefoldProperties.class); + registerSolverConfigsMethod.add("\n$T timefoldProperties = timefoldPropertiesResult.orElseGet($T::new);", + TimefoldProperties.class, TimefoldProperties.class); + + // Get the names of the solverConfigs + registerSolverConfigsMethod.add("\n$T solverConfigNames = getSolverConfigNames();\n", List.class); + + // If there are no solverConfigs... + registerSolverConfigsMethod.beginControlFlow("if (solverConfigNames.isEmpty())"); + // Create an empty one that can be used for injection + registerSolverConfigsMethod.add( + "\nbeanFactory.registerSingleton($S, new $T(beanFactory.getBeanClassLoader()));", + DEFAULT_SOLVER_CONFIG_NAME, SolverConfig.class); + registerSolverConfigsMethod.add("return;\n"); + registerSolverConfigsMethod.endControlFlow(); + + // If there is only a single solver + registerSolverConfigsMethod.beginControlFlow( + "if (timefoldProperties.getSolver() == null || timefoldProperties.getSolver().size() == 1)"); + // Use the default solver config name + registerSolverConfigsMethod.add( + "\nbeanFactory.registerSingleton($S, getSolverConfig((String) solverConfigNames.get(0)));", + DEFAULT_SOLVER_CONFIG_NAME); + registerSolverConfigsMethod.add("return;\n"); + registerSolverConfigsMethod.endControlFlow(); + + // Otherwise, for each solver... + registerSolverConfigsMethod.beginControlFlow("for (Object solverNameObj : solverConfigNames)"); + // Get the solver config with the given name + registerSolverConfigsMethod.add("\nString solverName = (String) solverNameObj;"); + registerSolverConfigsMethod.add( + "\n$T solverConfig = getSolverConfig(solverName);", + SolverConfig.class); + + // Create a solver manager from that solver config + registerSolverConfigsMethod.add("\n$T solverFactory = $T.create(solverConfig);", SolverFactory.class, + SolverFactory.class); + registerSolverConfigsMethod.add("\n$T solverManagerConfig = new $T();", SolverManagerConfig.class, + SolverManagerConfig.class); + registerSolverConfigsMethod.add("\n$T solverManagerProperties = timefoldProperties.getSolverManager();\n", + SolverManagerProperties.class); + registerSolverConfigsMethod.beginControlFlow( + "if (solverManagerProperties != null && solverManagerProperties.getParallelSolverCount() != null)"); + registerSolverConfigsMethod.add( + "\nsolverManagerConfig.setParallelSolverCount(solverManagerProperties.getParallelSolverCount());\n"); + registerSolverConfigsMethod.endControlFlow(); + + // Register that solver manager + registerSolverConfigsMethod.add( + "\nbeanFactory.registerSingleton(solverName, $T.create(solverFactory, solverManagerConfig));\n", + SolverManager.class); + registerSolverConfigsMethod.endControlFlow(); + + builder.addMethod(MethodSpec.methodBuilder("registerSolverConfigs") + .addModifiers(Modifier.PUBLIC) + .addModifiers(Modifier.STATIC) + .addParameter(Environment.class, "environment") + .addParameter(ConfigurableListableBeanFactory.class, "beanFactory") + .addCode(registerSolverConfigsMethod.build()) + .build()); + + builder.build(); + }); + + // Make spring call our generated class when the native image starts + beanFactoryInitializationCode.addInitializer(new DefaultMethodReference( + MethodSpec.methodBuilder("registerSolverConfigs") + .addModifiers(Modifier.PUBLIC) + .addModifiers(Modifier.STATIC) + .addParameter(Environment.class, "environment") + .addParameter(ConfigurableListableBeanFactory.class, "beanFactory") + .build(), + generatedClass.getName())); + } +} 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/TimefoldAutoConfiguration.java index dfed6a4df5..ce7ec6d99c 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/TimefoldAutoConfiguration.java @@ -10,7 +10,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,14 +24,10 @@ 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; @@ -40,23 +35,19 @@ 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.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.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -67,13 +58,8 @@ 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 @ConditionalOnClass({ SolverConfig.class, SolverFactory.class, ScoreManager.class, SolutionManager.class, SolverManager.class }) @@ -81,7 +67,8 @@ SolverManager.class }) @EnableConfigurationProperties({ TimefoldProperties.class }) public class TimefoldAutoConfiguration - implements BeanClassLoaderAware, ApplicationContextAware, EnvironmentAware, BeanFactoryPostProcessor { + implements BeanClassLoaderAware, ApplicationContextAware, EnvironmentAware, BeanFactoryInitializationAotProcessor, + BeanFactoryPostProcessor { private static final Log LOG = LogFactory.getLog(TimefoldAutoConfiguration.class); private static final String DEFAULT_SOLVER_CONFIG_NAME = "getSolverConfig"; @@ -123,8 +110,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 +120,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,6 +141,22 @@ 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 TimefoldAotContribution(solverConfigMap); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + Map solverConfigMap = getSolverConfigMap(); + if (solverConfigMap.isEmpty()) { + beanFactory.registerSingleton(DEFAULT_SOLVER_CONFIG_NAME, new SolverConfig(beanClassLoader)); + return; + } if (timefoldProperties.getSolver() == null || timefoldProperties.getSolver().size() == 1) { beanFactory.registerSingleton(DEFAULT_SOLVER_CONFIG_NAME, solverConfigMap.values().iterator().next()); @@ -287,13 +288,6 @@ 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) { List> timefoldFieldAnnotationList = entityScanner.findClassesWithAnnotation(PLANNING_ENTITY_FIELD_ANNOTATIONS); @@ -518,135 +512,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/TimefoldBeanFactory.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldBeanFactory.java new file mode 100644 index 0000000000..6f09e78321 --- /dev/null +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldBeanFactory.java @@ -0,0 +1,204 @@ +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 TimefoldAutoConfiguration} since + * {@link TimefoldAutoConfiguration} will not be available at runtime + * for a native image (since it is a {@link BeanFactoryInitializationAotProcessor}/ + * {@link BeanFactoryPostProcessor}). + */ +@Configuration +public class TimefoldBeanFactory 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 == 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/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..d64461ec84 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.TimefoldBenchmarkAutoConfiguration +ai.timefold.solver.spring.boot.autoconfigure.TimefoldBeanFactory \ 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/TimefoldAutoConfigurationTest.java index 5dc7e00b8a..a08228ce8e 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/TimefoldAutoConfigurationTest.java @@ -83,24 +83,25 @@ class TimefoldAutoConfigurationTest { public TimefoldAutoConfigurationTest() { contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) .withUserConfiguration(NormalSpringTestConfiguration.class); emptyContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) .withUserConfiguration(EmptySpringTestConfiguration.class); benchmarkContextRunner = new ApplicationContextRunner() .withConfiguration( - AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBenchmarkAutoConfiguration.class)) + AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class, + TimefoldBenchmarkAutoConfiguration.class)) .withUserConfiguration(NormalSpringTestConfiguration.class); gizmoContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) .withUserConfiguration(GizmoSpringTestConfiguration.class); chainedContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) .withUserConfiguration(ChainedSpringTestConfiguration.class); multimoduleRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) .withUserConfiguration(MultiModuleSpringTestConfiguration.class); allDefaultsFilteredClassLoader = new FilteredClassLoader(FilteredClassLoader.PackageFilter.of("ai.timefold.solver.test"), @@ -112,7 +113,7 @@ public TimefoldAutoConfigurationTest() { FilteredClassLoader.ClassPathResourceFilter.of( new ClassPathResource(TimefoldProperties.DEFAULT_SOLVER_CONFIG_URL))); noUserConfigurationContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)); } @Test 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/TimefoldMultipleSolverAutoConfigurationTest.java index 91dc5e3aa5..ec8f3ad311 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/TimefoldMultipleSolverAutoConfigurationTest.java @@ -69,30 +69,31 @@ class TimefoldMultipleSolverAutoConfigurationTest { public TimefoldMultipleSolverAutoConfigurationTest() { contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) .withUserConfiguration(NormalSpringTestConfiguration.class); emptyContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) .withUserConfiguration(EmptySpringTestConfiguration.class); benchmarkContextRunner = new ApplicationContextRunner() .withConfiguration( - AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBenchmarkAutoConfiguration.class)) + AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class, + TimefoldBenchmarkAutoConfiguration.class)) .withUserConfiguration(NormalSpringTestConfiguration.class); gizmoContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) .withUserConfiguration(GizmoSpringTestConfiguration.class); chainedContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) .withUserConfiguration(ChainedSpringTestConfiguration.class); multimoduleRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.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(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)); } @Test 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/TimefoldController.java b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldController.java new file mode 100644 index 0000000000..5bcab06c59 --- /dev/null +++ b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldController.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 TimefoldController { + private final SolverFactory solverFactory; + + public TimefoldController(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/TimefoldSpringBootApp.java b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSpringBootApp.java new file mode 100644 index 0000000000..fa9b392aef --- /dev/null +++ b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSpringBootApp.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 TimefoldSpringBootApp { + + public static void main(String[] args) { + SpringApplication.run(TimefoldSpringBootApp.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/TimefoldTestResourceIntegrationTest.java b/spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/TimefoldTestResourceIntegrationTest.java new file mode 100644 index 0000000000..5dc165b132 --- /dev/null +++ b/spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/TimefoldTestResourceIntegrationTest.java @@ -0,0 +1,48 @@ +package ai.timefold.solver.spring.boot.it; + +import java.util.List; + +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.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.web.reactive.server.WebTestClient; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class TimefoldTestResourceIntegrationTest { + + @LocalServerPort + String port; + + @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"); + + } +} From c33a0aae14b2df9a66d10e813217112e4b3b7103 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 1 Feb 2024 18:00:43 -0500 Subject: [PATCH 02/12] fix: Add support for Duration code serialization when generating Spring Native image --- .../spring/boot/autoconfigure/TimefoldAotContribution.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java index 03ae5b20cc..5bc12d4c03 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java @@ -2,6 +2,7 @@ import java.lang.reflect.Array; import java.lang.reflect.Field; +import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -138,6 +139,9 @@ public static String pojoToCode(Object pojo, // with the context class loader return "Thread.currentThread().getContextClassLoader()"; } + if (pojo instanceof Duration value) { + return Duration.class.getName() + ".ofNanos(" + value.toNanos() + "L)"; + } if (pojo.getClass().isEnum()) { // Use field access to read the enum Class enumClass = pojo.getClass(); From 22c583066d4a2227dd7883fa820012b7776618c8 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 6 Feb 2024 12:06:25 -0500 Subject: [PATCH 03/12] chore: Move Pojo serialization logic to it own class and add tests for it --- .../TimefoldAotContribution.java | 225 +------ .../boot/autoconfigure/util/PojoInliner.java | 386 +++++++++++ .../autoconfigure/util/PojoInlinerTest.java | 611 ++++++++++++++++++ 3 files changed, 1000 insertions(+), 222 deletions(-) create mode 100644 spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java create mode 100644 spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInlinerTest.java diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java index 5bc12d4c03..2d35bfc762 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java @@ -1,12 +1,8 @@ package ai.timefold.solver.spring.boot.autoconfigure; -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -19,8 +15,8 @@ import ai.timefold.solver.core.config.solver.SolverManagerConfig; import ai.timefold.solver.spring.boot.autoconfigure.config.SolverManagerProperties; import ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties; +import ai.timefold.solver.spring.boot.autoconfigure.util.PojoInliner; -import org.apache.commons.text.StringEscapeUtils; import org.springframework.aot.generate.DefaultMethodReference; import org.springframework.aot.generate.GeneratedClass; import org.springframework.aot.generate.GenerationContext; @@ -82,221 +78,6 @@ private void registerType(ReflectionHints reflectionHints, Class type) { // and surrounding it by double quotes. // - $T: Format as a fully qualified type, which allows you to use // classes without importing them. - - /** - * Serializes a Pojo to code that uses its no-args constructor - * and setters to create the object. - * - * @param pojo The object to be serialized. - * @param initializerCode The code block builder of the initializer - * @param complexPojoToIdentifier A map that stores objects already recorded - * @return A string that can be used in a {@link CodeBlock.Builder} to access the object - */ - public static String pojoToCode(Object pojo, - CodeBlock.Builder initializerCode, - Map complexPojoToIdentifier) { - // First, check for primitives - if (pojo == null) { - return "null"; - } - if (pojo instanceof Boolean value) { - return value.toString(); - } - if (pojo instanceof Byte value) { - return value.toString(); - } - if (pojo instanceof Character value) { - return "\\u" + Integer.toHexString(value | 0x10000).substring(1); - } - if (pojo instanceof Short value) { - return value.toString(); - } - if (pojo instanceof Integer value) { - return value.toString(); - } - if (pojo instanceof Long value) { - // Add long suffix to number string - return value + "L"; - } - if (pojo instanceof Float value) { - // Add float suffix to number string - return value + "f"; - } - if (pojo instanceof Double value) { - // Add double suffix to number string - return value + "d"; - } - - // Check for builtin classes - if (pojo instanceof String value) { - return "\"" + StringEscapeUtils.escapeJava(value) + "\""; - } - if (pojo instanceof Class value) { - return value.getName() + ".class"; - } - if (pojo instanceof ClassLoader) { - // We don't support serializing ClassLoaders, so replace it - // with the context class loader - return "Thread.currentThread().getContextClassLoader()"; - } - if (pojo instanceof Duration value) { - return Duration.class.getName() + ".ofNanos(" + value.toNanos() + "L)"; - } - if (pojo.getClass().isEnum()) { - // Use field access to read the enum - Class enumClass = pojo.getClass(); - Enum pojoEnum = (Enum) pojo; - return enumClass.getName() + "." + pojoEnum.name(); - } - return complexPojoToCode(pojo, initializerCode, complexPojoToIdentifier); - } - - /** - * Return a string that can be used in a {@link CodeBlock.Builder} to access a complex object - * - * @param pojo The object to be accessed - * @param complexPojoToIdentifier A Map from complex POJOs to their key in the map. - * @return A string that can be used in a {@link CodeBlock.Builder} to access the object. - */ - private static String getComplexPojo(Object pojo, Map complexPojoToIdentifier) { - return "((" + pojo.getClass().getName() + ") " + COMPLEX_POJO_MAP_FIELD_NAME + ".get(\"" - + complexPojoToIdentifier.get(pojo) + "\"))"; - } - - /** - * Serializes collections and complex POJOs to code - */ - private static String complexPojoToCode(Object pojo, CodeBlock.Builder initializerCode, - Map complexPojoToIdentifier) { - // If we already serialized the object, we should just return - // the code string - if (complexPojoToIdentifier.containsKey(pojo)) { - return getComplexPojo(pojo, complexPojoToIdentifier); - } - // Object is not serialized yet - // Create a new variable to store its value when setting its fields - String newIdentifier = "$obj" + complexPojoToIdentifier.size(); - complexPojoToIdentifier.put(pojo, newIdentifier); - initializerCode.add("\n$T $L;", pojo.getClass(), newIdentifier); - - // First, check if it is a collection type - if (pojo.getClass().isArray()) { - return arrayToCode(newIdentifier, pojo, initializerCode, complexPojoToIdentifier); - } - if (pojo instanceof List value) { - return listToCode(newIdentifier, value, initializerCode, complexPojoToIdentifier); - } - if (pojo instanceof Set value) { - return setToCode(newIdentifier, value, initializerCode, complexPojoToIdentifier); - } - if (pojo instanceof Map value) { - return mapToCode(newIdentifier, value, initializerCode, complexPojoToIdentifier); - } - - // Not a collection type, so serialize by creating a new instance and settings its fields - initializerCode.add("\n$L = new $T();", newIdentifier, pojo.getClass()); - initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); - setComplexPojoFields(pojo.getClass(), newIdentifier, pojo, initializerCode, complexPojoToIdentifier); - return getComplexPojo(pojo, complexPojoToIdentifier); - } - - private static String arrayToCode(String newIdentifier, Object array, CodeBlock.Builder initializerCode, - Map complexPojoToIdentifier) { - // Get the length of the array - int length = Array.getLength(array); - - // Create a new array from the component type with the given length - initializerCode.add("\n$L = new $T[$L];", newIdentifier, array.getClass().getComponentType(), Integer.toString(length)); - initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); - for (int i = 0; i < length; i++) { - // Set the elements of the array - initializerCode.add("\n$L[$L] = $L;", - newIdentifier, - Integer.toString(i), - pojoToCode(Array.get(array, i), initializerCode, complexPojoToIdentifier)); - } - return getComplexPojo(array, complexPojoToIdentifier); - } - - private static String listToCode(String newIdentifier, List list, CodeBlock.Builder initializerCode, - Map complexPojoToIdentifier) { - // Create an ArrayList - initializerCode.add("\n$L = new $T($L);", newIdentifier, ArrayList.class, Integer.toString(list.size())); - initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); - for (Object item : list) { - // Add each item of the list to the ArrayList - initializerCode.add("\n$L.add($L);", - newIdentifier, - pojoToCode(item, initializerCode, complexPojoToIdentifier)); - } - return getComplexPojo(list, complexPojoToIdentifier); - } - - private static String setToCode(String newIdentifier, Set set, CodeBlock.Builder initializerCode, - Map complexPojoToIdentifier) { - // Create a new HashSet - initializerCode.add("\n$L = new $T($L);", newIdentifier, HashSet.class, Integer.toString(set.size())); - initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); - for (Object item : set) { - // Add each item of the set to the HashSet - initializerCode.add("\n$L.add($L);", - newIdentifier, - pojoToCode(item, initializerCode, complexPojoToIdentifier)); - } - return getComplexPojo(set, complexPojoToIdentifier); - } - - private static String mapToCode(String newIdentifier, Map map, CodeBlock.Builder initializerCode, - Map complexPojoToIdentifier) { - // Create a HashMap - initializerCode.add("\n$L = new $T($L);", newIdentifier, HashMap.class, Integer.toString(map.size())); - initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); - for (Map.Entry entry : map.entrySet()) { - // Put each entry of the map into the HashMap - initializerCode.add("\n$L.put($L,$L);", - newIdentifier, - pojoToCode(entry.getKey(), initializerCode, complexPojoToIdentifier), - pojoToCode(entry.getValue(), initializerCode, complexPojoToIdentifier)); - } - return getComplexPojo(map, complexPojoToIdentifier); - } - - /** - * Sets the fields of pojo declared in pojoClass and all its superclasses. - * - * @param pojoClass A class assignable to pojo containing some of its fields. - * @param identifier The name of the variable storing the serialized pojo. - * @param pojo The object being serialized. - * @param initializerCode The {@link CodeBlock.Builder} to use to generate code in the initializer. - * @param complexPojoToIdentifier A map from complex POJOs to their variable name. - */ - private static void setComplexPojoFields(Class pojoClass, String identifier, Object pojo, - CodeBlock.Builder initializerCode, Map complexPojoToIdentifier) { - if (pojoClass == Object.class) { - // We are the top-level, no more fields to set - return; - } - for (Field field : pojo.getClass().getDeclaredFields()) { - if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) { - // We do not want to write static fields - continue; - } - // Set the field accessible so we can read its value - field.setAccessible(true); - try { - // Convert the field value to code, and call the setter - // corresponding to the field with the serialized field value. - initializerCode.add("\n$L.set$L$L($L);", identifier, - Character.toUpperCase(field.getName().charAt(0)), - field.getName().substring(1), - pojoToCode(field.get(pojo), initializerCode, complexPojoToIdentifier)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - setComplexPojoFields(pojoClass.getSuperclass(), identifier, pojo, initializerCode, complexPojoToIdentifier); - } - @Override public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { var reflectionHints = generationContext.getRuntimeHints().reflection(); @@ -319,15 +100,15 @@ public void applyTo(GenerationContext generationContext, BeanFactoryInitializati // Create a generated class to hold all the solver configs GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("timefold-aot", builder -> { + PojoInliner pojoInliner = new PojoInliner(); builder.addField(Map.class, "solverConfigMap", Modifier.STATIC); builder.addField(Map.class, COMPLEX_POJO_MAP_FIELD_NAME, Modifier.STATIC); // Handwrite the SolverConfig map in the initializer CodeBlock.Builder staticInitializer = CodeBlock.builder(); - Map complexPojoToIdentifier = new IdentityHashMap<>(); staticInitializer.add("$L = new $T();", COMPLEX_POJO_MAP_FIELD_NAME, HashMap.class); staticInitializer.add("\nsolverConfigMap = $L;", - complexPojoToCode(solverConfigMap, staticInitializer, complexPojoToIdentifier)); + pojoInliner.getInlinedPojo(solverConfigMap, staticInitializer)); builder.addStaticBlock(staticInitializer.build()); // getSolverConfig fetches the SolverConfig with the given name from the map diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java new file mode 100644 index 0000000000..3fd4663760 --- /dev/null +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java @@ -0,0 +1,386 @@ +package ai.timefold.solver.spring.boot.autoconfigure.util; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.RecordComponent; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; + +import org.apache.commons.text.StringEscapeUtils; +import org.springframework.javapoet.CodeBlock; + +public final class PojoInliner { + static final String COMPLEX_POJO_MAP_FIELD_NAME = "$pojoMap"; + private final Map complexPojoToIdentifier = new IdentityHashMap<>(); + private final Set possibleCircularRecordReferenceSet = Collections.newSetFromMap(new IdentityHashMap<>()); + + // The code below uses CodeBlock.Builder to generate the Java file + // that stores the SolverConfig. + // CodeBlock.Builder.add supports different kinds of formatting args. + // The ones we use are: + // - $L: Format as is (i.e. literal replacement). + // - $S: Format as a Java String, doing the necessary escapes + // and surrounding it by double quotes. + // - $T: Format as a fully qualified type, which allows you to use + // classes without importing them. + + /** + * Serializes a Pojo to code that uses its no-args constructor + * and setters to create the object. + * + * @param pojo The object to be serialized. + * @param initializerCode The code block builder of the initializer + * @return A string that can be used in a {@link CodeBlock.Builder} to access the object + */ + public String getInlinedPojo(Object pojo, + CodeBlock.Builder initializerCode) { + // First, check for primitives + if (pojo == null) { + return "null"; + } + if (pojo instanceof Boolean value) { + return value.toString(); + } + if (pojo instanceof Byte value) { + // Cast to byte + return "((byte) " + value + ")"; + } + if (pojo instanceof Character value) { + return "'\\u" + Integer.toHexString(value | 0x10000).substring(1) + "'"; + } + if (pojo instanceof Short value) { + // Cast to short + return "((short) " + value + ")"; + } + if (pojo instanceof Integer value) { + return value.toString(); + } + if (pojo instanceof Long value) { + // Add long suffix to number string + return value + "L"; + } + if (pojo instanceof Float value) { + // Add float suffix to number string + return value + "f"; + } + if (pojo instanceof Double value) { + // Add double suffix to number string + return value + "d"; + } + + // Check for builtin classes + if (pojo instanceof String value) { + return "\"" + StringEscapeUtils.escapeJava(value) + "\""; + } + if (pojo instanceof Class value) { + if (!Modifier.isPublic(value.getModifiers())) { + throw new IllegalArgumentException("Cannot serialize (" + value + ") because it is not a public class."); + } + return value.getCanonicalName() + ".class"; + } + if (pojo instanceof ClassLoader) { + // We don't support serializing ClassLoaders, so replace it + // with the context class loader + return "Thread.currentThread().getContextClassLoader()"; + } + if (pojo instanceof Duration value) { + return Duration.class.getCanonicalName() + ".ofNanos(" + value.toNanos() + "L)"; + } + if (pojo.getClass().isEnum()) { + // Use field access to read the enum + Class enumClass = pojo.getClass(); + if (!Modifier.isPublic(enumClass.getModifiers())) { + throw new IllegalArgumentException( + "Cannot serialize (" + pojo + ") because its type (" + enumClass + ") is not a public class."); + } + Enum pojoEnum = (Enum) pojo; + return enumClass.getCanonicalName() + "." + pojoEnum.name(); + } + return getInlinedComplexPojo(pojo, initializerCode); + } + + /** + * Return a string that can be used in a {@link CodeBlock.Builder} to access a complex object + * + * @param pojo The object to be accessed + * @return A string that can be used in a {@link CodeBlock.Builder} to access the object. + */ + private String getPojoFromMap(Object pojo) { + return CodeBlock.builder().add("(($T) $L.get($S))", + pojo.getClass(), + COMPLEX_POJO_MAP_FIELD_NAME, + complexPojoToIdentifier.get(pojo)).build().toString(); + } + + /** + * Serializes collections and complex POJOs to code + */ + private String getInlinedComplexPojo(Object pojo, CodeBlock.Builder initializerCode) { + if (possibleCircularRecordReferenceSet.contains(pojo)) { + // Records do not have a no-args constructor, so we cannot safely serialize self-references in records + // as we cannot do a map lookup before the record is created. + throw new IllegalArgumentException( + "Cannot serialize record (" + pojo + ") because it is a record containing contains a circular reference."); + } + + // If we already serialized the object, we should just return + // the code string + if (complexPojoToIdentifier.containsKey(pojo)) { + return getPojoFromMap(pojo); + } + if (pojo instanceof Record value) { + // Records must set all fields at initialization time, + // so we delay the declaration of its variable + return getInlinedRecord(value, initializerCode); + } + // Object is not serialized yet + // Create a new variable to store its value when setting its fields + String newIdentifier = "$obj" + complexPojoToIdentifier.size(); + complexPojoToIdentifier.put(pojo, newIdentifier); + + // First, check if it is a collection type + if (pojo.getClass().isArray()) { + return getInlinedArray(newIdentifier, pojo, initializerCode); + } + if (pojo instanceof List value) { + return getInlinedList(newIdentifier, value, initializerCode); + } + if (pojo instanceof Set value) { + return getInlinedSet(newIdentifier, value, initializerCode); + } + if (pojo instanceof Map value) { + return getInlinedMap(newIdentifier, value, initializerCode); + } + + // Not a collection or record type, so serialize by creating a new instance and settings its fields + if (!Modifier.isPublic(pojo.getClass().getModifiers())) { + throw new IllegalArgumentException("Cannot serialize (" + pojo + ") because its type (" + pojo.getClass() + + ") is not public."); + } + initializerCode.add("\n$T $L;", pojo.getClass(), newIdentifier); + try { + Constructor constructor = pojo.getClass().getConstructor(); + if (!Modifier.isPublic(constructor.getModifiers())) { + throw new IllegalArgumentException("Cannot serialize (" + pojo + ") because its type's (" + pojo.getClass() + + ") no-args constructor is not public."); + } + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Cannot serialize (" + pojo + ") because its type (" + pojo.getClass() + + ") does not have a public no-args constructor."); + } + initializerCode.add("\n$L = new $T();", newIdentifier, pojo.getClass()); + initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + inlineFieldsOfPojo(pojo.getClass(), newIdentifier, pojo, initializerCode); + return getPojoFromMap(pojo); + } + + private String getInlinedArray(String newIdentifier, Object array, CodeBlock.Builder initializerCode) { + Class componentType = array.getClass().getComponentType(); + if (!Modifier.isPublic(componentType.getModifiers())) { + throw new IllegalArgumentException( + "Cannot serialize array of type (" + componentType + ") because (" + componentType + ") is not public."); + } + initializerCode.add("\n$T $L;", array.getClass(), newIdentifier); + + // Get the length of the array + int length = Array.getLength(array); + + // Create a new array from the component type with the given length + initializerCode.add("\n$L = new $T[$L];", newIdentifier, componentType, Integer.toString(length)); + initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + for (int i = 0; i < length; i++) { + // Set the elements of the array + initializerCode.add("\n$L[$L] = $L;", + newIdentifier, + Integer.toString(i), + getInlinedPojo(Array.get(array, i), initializerCode)); + } + return getPojoFromMap(array); + } + + private String getInlinedList(String newIdentifier, List list, CodeBlock.Builder initializerCode) { + initializerCode.add("\n$T $L;", List.class, newIdentifier); + + // Create an ArrayList + initializerCode.add("\n$L = new $T($L);", newIdentifier, ArrayList.class, Integer.toString(list.size())); + initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + for (Object item : list) { + // Add each item of the list to the ArrayList + initializerCode.add("\n$L.add($L);", + newIdentifier, + getInlinedPojo(item, initializerCode)); + } + return getPojoFromMap(list); + } + + private String getInlinedSet(String newIdentifier, Set set, CodeBlock.Builder initializerCode) { + initializerCode.add("\n$T $L;", Set.class, newIdentifier); + + // Create a new HashSet + initializerCode.add("\n$L = new $T($L);", newIdentifier, LinkedHashSet.class, Integer.toString(set.size())); + initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + for (Object item : set) { + // Add each item of the set to the HashSet + initializerCode.add("\n$L.add($L);", + newIdentifier, + getInlinedPojo(item, initializerCode)); + } + return getPojoFromMap(set); + } + + private String getInlinedMap(String newIdentifier, Map map, CodeBlock.Builder initializerCode) { + initializerCode.add("\n$T $L;", Map.class, newIdentifier); + + // Create a HashMap + initializerCode.add("\n$L = new $T($L);", newIdentifier, LinkedHashMap.class, Integer.toString(map.size())); + initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + for (Map.Entry entry : map.entrySet()) { + // Put each entry of the map into the HashMap + initializerCode.add("\n$L.put($L, $L);", + newIdentifier, + getInlinedPojo(entry.getKey(), initializerCode), + getInlinedPojo(entry.getValue(), initializerCode)); + } + return getPojoFromMap(map); + } + + private String getInlinedRecord(Record record, CodeBlock.Builder initializerCode) { + possibleCircularRecordReferenceSet.add(record); + Class recordClass = record.getClass(); + if (!Modifier.isPublic(recordClass.getModifiers())) { + throw new IllegalArgumentException( + "Cannot serialize record (" + record + ") because its type (" + recordClass + ") is not public."); + } + + RecordComponent[] recordComponents = recordClass.getRecordComponents(); + String[] componentAccessors = new String[recordComponents.length]; + for (int i = 0; i < recordComponents.length; i++) { + Object value; + Class serializedType = getSerializedType(recordComponents[i].getType()); + if (!recordComponents[i].getType().equals(serializedType)) { + throw new IllegalArgumentException( + "Cannot serialize type (" + recordClass + ") as its component (" + recordComponents[i].getName() + + ") uses an implementation of a collection (" + + recordComponents[i].getType() + ") instead of the interface type (" + serializedType + ")."); + } + try { + value = recordComponents[i].getAccessor().invoke(record); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + try { + componentAccessors[i] = getInlinedPojo(value, initializerCode); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Cannot serialize record (" + record + ") because the value (" + + value + ") for its component (" + recordComponents[i].getName() + ") is not serializable.", e); + } + } + // All components serialized, so no circular references + possibleCircularRecordReferenceSet.remove(record); + StringBuilder constructorArgs = new StringBuilder(); + for (String componentAccessor : componentAccessors) { + constructorArgs.append(componentAccessor).append(", "); + } + if (componentAccessors.length != 0) { + constructorArgs.delete(constructorArgs.length() - 2, constructorArgs.length()); + } + String newIdentifier = "$obj" + complexPojoToIdentifier.size(); + complexPojoToIdentifier.put(record, newIdentifier); + initializerCode.add("\n$T $L = new $T($L);", recordClass, newIdentifier, recordClass, constructorArgs.toString()); + initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + return getPojoFromMap(record); + } + + private Class getSerializedType(Class query) { + if (List.class.isAssignableFrom(query)) { + return List.class; + } + if (Set.class.isAssignableFrom(query)) { + return Set.class; + } + if (Map.class.isAssignableFrom(query)) { + return Map.class; + } + return query; + } + + /** + * Sets the fields of pojo declared in pojoClass and all its superclasses. + * + * @param pojoClass A class assignable to pojo containing some of its fields. + * @param identifier The name of the variable storing the serialized pojo. + * @param pojo The object being serialized. + * @param initializerCode The {@link CodeBlock.Builder} to use to generate code in the initializer. + */ + private void inlineFieldsOfPojo(Class pojoClass, String identifier, Object pojo, + CodeBlock.Builder initializerCode) { + if (pojoClass == Object.class) { + // We are the top-level, no more fields to set + return; + } + Field[] fields = pojoClass.getDeclaredFields(); + // Sort by name to guarantee a consistent ordering + Arrays.sort(fields, Comparator.comparing(Field::getName)); + for (Field field : fields) { + if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) { + // We do not want to write static fields + continue; + } + // Set the field accessible so we can read its value + field.setAccessible(true); + Class serializedType = getSerializedType(field.getType()); + Method setterMethod = ReflectionHelper.getSetterMethod(pojoClass, serializedType, field.getName()); + // setterMethod guaranteed to be public + if (setterMethod == null) { + if (!field.getType().equals(serializedType)) { + throw new IllegalArgumentException( + "Cannot serialize type (" + pojoClass + ") as its field (" + field.getName() + + ") uses an implementation of a collection (" + + field.getType() + ") instead of the interface type (" + serializedType + ")."); + } + throw new IllegalArgumentException( + "Cannot serialize type (" + pojoClass + ") as it is missing a public setter method for field (" + + field.getName() + ") of type (" + field.getType() + ")."); + } + Object value; + try { + value = field.get(pojo); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + + try { + // Convert the field value to code, and call the setter + // corresponding to the field with the serialized field value. + initializerCode.add("\n$L.$L($L);", identifier, + setterMethod.getName(), + getInlinedPojo(value, initializerCode)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Cannot serialize object (" + pojo + ") because the value (" + value + + ") for its field (" + field.getName() + ") is not serializable.", e); + } + } + try { + inlineFieldsOfPojo(pojoClass.getSuperclass(), identifier, pojo, initializerCode); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Cannot serialize type (" + pojoClass + ") because its superclass (" + + pojoClass.getSuperclass() + ") is not serializable.", e); + } + + } +} diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInlinerTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInlinerTest.java new file mode 100644 index 0000000000..dc90753c57 --- /dev/null +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInlinerTest.java @@ -0,0 +1,611 @@ +package ai.timefold.solver.spring.boot.autoconfigure.util; + +import static ai.timefold.solver.spring.boot.autoconfigure.util.PojoInliner.COMPLEX_POJO_MAP_FIELD_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import ai.timefold.solver.core.config.solver.EnvironmentMode; + +import org.junit.jupiter.api.Test; +import org.springframework.javapoet.CodeBlock; + +public class PojoInlinerTest { + private static class PrivatePojo { + @Override + public String toString() { + return "PrivatePojo()"; + } + } + + private record PrivateRecord() { + } + + private enum PrivateEnum { + VALUE + } + + public static class BasicPojo { + BasicPojo parentPojo; + int id; + String name; + + public BasicPojo() { + } + + public BasicPojo(BasicPojo parentPojo, int id, String name) { + this.parentPojo = parentPojo; + this.id = id; + this.name = name; + } + + public BasicPojo getParentPojo() { + return parentPojo; + } + + public void setParentPojo(BasicPojo parentPojo) { + this.parentPojo = parentPojo; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + public record RecordPojo(String operation, int recordId, BasicPojo pojo) { + } + + public static class LinkedRecordPojoReference { + LinkedRecordPojo reference; + + public LinkedRecordPojo getReference() { + return reference; + } + + public void setReference(LinkedRecordPojo reference) { + this.reference = reference; + } + + @Override + public String toString() { + return "LinkedRecordPojoReference()"; + } + } + + public record LinkedRecordPojo(LinkedRecordPojoReference next) { + public static LinkedRecordPojo circular() { + LinkedRecordPojoReference nextField = new LinkedRecordPojoReference(); + LinkedRecordPojo out = new LinkedRecordPojo(nextField); + nextField.setReference(out); + return out; + } + + public static LinkedRecordPojo nonCircular() { + LinkedRecordPojoReference nextField = new LinkedRecordPojoReference(); + nextField.setReference(new LinkedRecordPojo(null)); + return new LinkedRecordPojo(nextField); + } + } + + public record ArrayListRecord(ArrayList arrayList) { + } + + public static class ArrayListPojo { + ArrayList arrayList; + + public ArrayList getArrayList() { + return arrayList; + } + + public void setArrayList(ArrayList arrayList) { + this.arrayList = arrayList; + } + + @Override + public String toString() { + return "ArrayListPojo{" + + "arrayList=" + arrayList + + '}'; + } + } + + public static class NotPojo { + int aFieldWithSetter; + int bFieldWithoutSetter; + + public NotPojo() { + + } + + public NotPojo(int aFieldWithSetter, int bFieldWithoutSetter) { + this.aFieldWithSetter = aFieldWithSetter; + this.bFieldWithoutSetter = bFieldWithoutSetter; + } + + public int getAFieldWithSetter() { + return aFieldWithSetter; + } + + public void setAFieldWithSetter(int aFieldWithSetter) { + this.aFieldWithSetter = aFieldWithSetter; + } + + public int getBFieldWithoutSetter() { + return bFieldWithoutSetter; + } + } + + public static class PrivateSetterPojo { + int aFieldWithSetter; + int bFieldWithSetter; + + public PrivateSetterPojo() { + + } + + public PrivateSetterPojo(int aFieldWithSetter, int bFieldWithSetter) { + this.aFieldWithSetter = aFieldWithSetter; + this.bFieldWithSetter = bFieldWithSetter; + } + + public int getAFieldWithSetter() { + return aFieldWithSetter; + } + + public void setAFieldWithSetter(int aFieldWithSetter) { + this.aFieldWithSetter = aFieldWithSetter; + } + + public int getBFieldWithoutSetter() { + return bFieldWithSetter; + } + + private void setBFieldWithSetter(int bFieldWithSetter) { + this.bFieldWithSetter = bFieldWithSetter; + } + } + + public static class ExtendedPojo extends BasicPojo { + private String additionalField; + + public String getAdditionalField() { + return additionalField; + } + + public void setAdditionalField(String additionalField) { + this.additionalField = additionalField; + } + } + + public static class ExtendedNotPojo extends PrivateSetterPojo { + private String additionalField; + + public String getAdditionalField() { + return additionalField; + } + + public void setAdditionalField(String additionalField) { + this.additionalField = additionalField; + } + } + + void assertBuilder(CodeBlock.Builder builder, String expected) { + assertThat(builder.build().toString().trim()).isEqualTo(expected.trim()); + } + + private String getPojo(int id, Object value) { + return CodeBlock.builder().add("(($T) $L.get($S))", + value.getClass(), + COMPLEX_POJO_MAP_FIELD_NAME, + "$obj" + id).build().toString(); + } + + void assertAccessor(String accessor, int id, Object value) { + assertThat(accessor).isEqualTo(getPojo(id, value)); + } + + @Test + void inlinePrivateClasses() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + + assertThatCode(() -> inliner.getInlinedPojo(PrivatePojo.class, builder)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize (" + PrivatePojo.class + ") because it is not a public class."); + + assertThatCode(() -> inliner.getInlinedPojo(new PrivatePojo(), builder)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize (" + new PrivatePojo() + ") because its type (" + PrivatePojo.class + + ") is not public."); + + assertThatCode(() -> inliner.getInlinedPojo(new PrivateRecord(), builder)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize record (" + new PrivateRecord() + ") because its type (" + PrivateRecord.class + + ") is not public."); + + assertThatCode(() -> inliner.getInlinedPojo(PrivateEnum.VALUE, builder)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize (" + PrivateEnum.VALUE + ") because its type (" + PrivateEnum.class + + ") is not a public class."); + } + + @Test + void inlinePrivateSetter() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + + assertThatCode(() -> inliner.getInlinedPojo(new PrivateSetterPojo(1, 2), builder)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize type (" + PrivateSetterPojo.class + + ") as it is missing a public setter method for field (bFieldWithSetter) of type (int)."); + } + + @Test + void inlineTypeUsingInterfaceImpl() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + ArrayListPojo pojo = new ArrayListPojo(); + pojo.setArrayList(new ArrayList<>()); + assertThatCode(() -> inliner.getInlinedPojo(pojo, builder)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize type (" + ArrayListPojo.class + + ") as its field (arrayList) uses an implementation of a collection (" + ArrayList.class + + ") instead of the interface type (" + List.class + ")."); + + ArrayListRecord record = new ArrayListRecord(new ArrayList<>()); + assertThatCode(() -> inliner.getInlinedPojo(record, builder)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize type (" + ArrayListRecord.class + + ") as its component (arrayList) uses an implementation of a collection (" + ArrayList.class + + ") instead of the interface type (" + List.class + ")."); + } + + @Test + void inlineNotPojo() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + NotPojo pojo = new NotPojo(1, 2); + assertThatCode(() -> inliner.getInlinedPojo(pojo, builder)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize type (" + NotPojo.class + + ") as it is missing a public setter method for field (bFieldWithoutSetter) of type (int)."); + } + + @Test + void inlineExtendedNotPojo() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + + assertThatCode(() -> inliner.getInlinedPojo(new ExtendedNotPojo(), builder)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize type (" + ExtendedNotPojo.class + ") because its superclass (" + + PrivateSetterPojo.class + ") is not serializable.") + .cause() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize type (" + PrivateSetterPojo.class + + ") as it is missing a public setter method for field (bFieldWithSetter) of type (int)."); + } + + @Test + void inlineCircularRecord() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + + LinkedRecordPojo pojo = LinkedRecordPojo.circular(); + + assertThatCode(() -> inliner.getInlinedPojo(pojo, builder)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize record (" + pojo + ") because the value (" + pojo.next + + ") for its component (next) is not serializable.") + .cause() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize object (" + pojo.next + ") because the value (" + pojo + + ") for its field (reference) is not serializable.") + .cause() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot serialize record (" + pojo + + ") because it is a record containing contains a circular reference."); + } + + @Test + void inlinePrimitives() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + + // null + assertThat(inliner.getInlinedPojo(null, builder)).isEqualTo("null"); + assertBuilder(builder, ""); + + // numbers + assertThat(inliner.getInlinedPojo(true, builder)).isEqualTo("true"); + assertBuilder(builder, ""); + assertThat(inliner.getInlinedPojo(false, builder)).isEqualTo("false"); + assertBuilder(builder, ""); + assertThat(inliner.getInlinedPojo((byte) 1, builder)).isEqualTo("((byte) 1)"); + assertBuilder(builder, ""); + assertThat(inliner.getInlinedPojo((short) 1, builder)).isEqualTo("((short) 1)"); + assertBuilder(builder, ""); + assertThat(inliner.getInlinedPojo(1, builder)).isEqualTo("1"); + assertBuilder(builder, ""); + assertThat(inliner.getInlinedPojo(1L, builder)).isEqualTo("1L"); + assertBuilder(builder, ""); + assertThat(inliner.getInlinedPojo(1f, builder)).isEqualTo("1.0f"); + assertBuilder(builder, ""); + assertThat(inliner.getInlinedPojo(1d, builder)).isEqualTo("1.0d"); + assertBuilder(builder, ""); + + // Strings and chars + assertThat(inliner.getInlinedPojo('a', builder)).isEqualTo("'\\u0061'"); + assertBuilder(builder, ""); + assertThat(inliner.getInlinedPojo("my\nmultiline\nstring", builder)).isEqualTo("\"my\\nmultiline\\nstring\""); + assertBuilder(builder, ""); + } + + @Test + void inlineObjectPrimitives() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + + // Classes + assertThat(inliner.getInlinedPojo(PojoInliner.class, builder)) + .isEqualTo(PojoInliner.class.getCanonicalName() + ".class"); + assertBuilder(builder, ""); + + // Enums + assertThat(inliner.getInlinedPojo(EnvironmentMode.REPRODUCIBLE, builder)) + .isEqualTo(EnvironmentMode.class.getCanonicalName() + "." + EnvironmentMode.REPRODUCIBLE.name()); + assertBuilder(builder, ""); + + // ClassLoader + assertThat(inliner.getInlinedPojo(Thread.currentThread().getContextClassLoader(), builder)) + .isEqualTo("Thread.currentThread().getContextClassLoader()"); + assertBuilder(builder, ""); + } + + @Test + void inlinePrimitiveArray() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + int[] pojo = new int[] { 1, 2, 3, 4, 5 }; + String accessor = inliner.getInlinedPojo(pojo, builder); + assertBuilder(builder, + """ + int[] $obj0; + $obj0 = new int[5]; + $pojoMap.put("$obj0", $obj0); + $obj0[0] = 1; + $obj0[1] = 2; + $obj0[2] = 3; + $obj0[3] = 4; + $obj0[4] = 5; + """); + assertAccessor(accessor, 0, pojo); + } + + @Test + void inlineObjectArray() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + Object[] pojo = new Object[3]; + pojo[0] = null; + pojo[1] = pojo; + pojo[2] = "Item"; + String accessor = inliner.getInlinedPojo(pojo, builder); + assertBuilder(builder, + """ + java.lang.Object[] $obj0; + $obj0 = new java.lang.Object[3]; + $pojoMap.put("$obj0", $obj0); + $obj0[0] = null; + $obj0[1] = %s; + $obj0[2] = \"Item\"; + """.formatted(getPojo(0, pojo))); + assertAccessor(accessor, 0, pojo); + } + + @Test + void inlineIntList() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + List pojo = List.of(1, 2, 3); + String accessor = inliner.getInlinedPojo(pojo, builder); + assertBuilder(builder, + """ + java.util.List $obj0; + $obj0 = new java.util.ArrayList(3); + $pojoMap.put("$obj0", $obj0); + $obj0.add(1); + $obj0.add(2); + $obj0.add(3); + """); + assertAccessor(accessor, 0, pojo); + } + + @Test + void inlineIntSet() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + Set pojo = new LinkedHashSet<>(3); + pojo.add(1); + pojo.add(2); + pojo.add(3); + String accessor = inliner.getInlinedPojo(pojo, builder); + assertBuilder(builder, + """ + java.util.Set $obj0; + $obj0 = new java.util.LinkedHashSet(3); + $pojoMap.put("$obj0", $obj0); + $obj0.add(1); + $obj0.add(2); + $obj0.add(3); + """); + assertAccessor(accessor, 0, pojo); + } + + @Test + void inlineIntStringMap() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + Map pojo = new LinkedHashMap<>(3); + pojo.put(1, "a"); + pojo.put(2, "b"); + pojo.put(3, "c"); + String accessor = inliner.getInlinedPojo(pojo, builder); + assertBuilder(builder, + """ + java.util.Map $obj0; + $obj0 = new java.util.LinkedHashMap(3); + $pojoMap.put("$obj0", $obj0); + $obj0.put(1, \"a\"); + $obj0.put(2, \"b\"); + $obj0.put(3, \"c\"); + """); + assertAccessor(accessor, 0, pojo); + } + + @Test + void inlinePojo() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + BasicPojo pojo = new BasicPojo(new BasicPojo(null, 0, "parent"), 1, "child"); + String accessor = inliner.getInlinedPojo(pojo, builder); + String expected = """ + %s $obj0; + $obj0 = new %s(); + $pojoMap.put("$obj0", $obj0); + $obj0.setId(1); + $obj0.setName("child"); + %s $obj1; + $obj1 = new %s(); + $pojoMap.put("$obj1", $obj1); + $obj1.setId(0); + $obj1.setName("parent"); + $obj1.setParentPojo(null); + $obj0.setParentPojo(%s); + """.formatted(BasicPojo.class.getCanonicalName(), + BasicPojo.class.getCanonicalName(), + BasicPojo.class.getCanonicalName(), + BasicPojo.class.getCanonicalName(), + getPojo(1, pojo.getParentPojo())); + assertBuilder(builder, expected); + assertAccessor(accessor, 0, pojo); + String parentAccessor = inliner.getInlinedPojo(pojo.getParentPojo(), builder); + assertBuilder(builder, expected); + assertAccessor(parentAccessor, 1, pojo.getParentPojo()); + } + + @Test + void inlineExtendedPojo() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + ExtendedPojo pojo = new ExtendedPojo(); + pojo.setAdditionalField("newField"); + pojo.setId(1); + pojo.setName("child"); + pojo.setParentPojo(new BasicPojo(null, 0, "parent")); + String accessor = inliner.getInlinedPojo(pojo, builder); + String expected = """ + %s $obj0; + $obj0 = new %s(); + $pojoMap.put("$obj0", $obj0); + $obj0.setAdditionalField("newField"); + $obj0.setId(1); + $obj0.setName("child"); + %s $obj1; + $obj1 = new %s(); + $pojoMap.put("$obj1", $obj1); + $obj1.setId(0); + $obj1.setName("parent"); + $obj1.setParentPojo(null); + $obj0.setParentPojo(%s); + """.formatted(ExtendedPojo.class.getCanonicalName(), + ExtendedPojo.class.getCanonicalName(), + BasicPojo.class.getCanonicalName(), + BasicPojo.class.getCanonicalName(), + getPojo(1, pojo.getParentPojo())); + assertBuilder(builder, expected); + assertAccessor(accessor, 0, pojo); + String parentAccessor = inliner.getInlinedPojo(pojo.getParentPojo(), builder); + assertBuilder(builder, expected); + assertAccessor(parentAccessor, 1, pojo.getParentPojo()); + } + + @Test + void inlineRecord() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + BasicPojo pojo = new BasicPojo(null, 0, "name"); + RecordPojo recordPojo = new RecordPojo("INSERT", 0, pojo); + String accessor = inliner.getInlinedPojo(recordPojo, builder); + String expected = """ + %s $obj0; + $obj0 = new %s(); + $pojoMap.put("$obj0", $obj0); + $obj0.setId(0); + $obj0.setName("name"); + $obj0.setParentPojo(null); + %s $obj1 = new %s("INSERT", 0, %s); + $pojoMap.put("$obj1", $obj1); + """.formatted(BasicPojo.class.getCanonicalName(), + BasicPojo.class.getCanonicalName(), + RecordPojo.class.getCanonicalName(), + RecordPojo.class.getCanonicalName(), + getPojo(0, recordPojo.pojo())); + assertBuilder(builder, expected); + assertAccessor(accessor, 1, recordPojo); + String partAccessor = inliner.getInlinedPojo(recordPojo.pojo(), builder); + assertBuilder(builder, expected); + assertAccessor(partAccessor, 0, recordPojo.pojo()); + } + + @Test + void inlineNonCircularRecord() { + PojoInliner inliner = new PojoInliner(); + CodeBlock.Builder builder = CodeBlock.builder(); + LinkedRecordPojo pojo = LinkedRecordPojo.nonCircular(); + String expected = """ + %s $obj0; + $obj0 = new %s(); + $pojoMap.put("$obj0", $obj0); + %s $obj1 = new %s(null); + $pojoMap.put("$obj1", $obj1); + $obj0.setReference(%s); + %s $obj2 = new %s(%s); + $pojoMap.put("$obj2", $obj2); + """.formatted(LinkedRecordPojoReference.class.getCanonicalName(), + LinkedRecordPojoReference.class.getCanonicalName(), + LinkedRecordPojo.class.getCanonicalName(), + LinkedRecordPojo.class.getCanonicalName(), + getPojo(1, pojo.next().getReference()), + LinkedRecordPojo.class.getCanonicalName(), + LinkedRecordPojo.class.getCanonicalName(), + getPojo(0, pojo.next())); + String accessor = inliner.getInlinedPojo(pojo, builder); + assertBuilder(builder, expected); + assertAccessor(accessor, 2, pojo); + String nextAccessor = inliner.getInlinedPojo(pojo.next(), builder); + assertBuilder(builder, expected); + assertAccessor(nextAccessor, 0, pojo.next()); + String referenceAccessor = inliner.getInlinedPojo(pojo.next().getReference(), builder); + assertBuilder(builder, expected); + assertAccessor(referenceAccessor, 1, pojo.next().getReference()); + } +} From 9fdd731ef389910404daa2c9c97dad6096616731 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 6 Feb 2024 13:01:55 -0500 Subject: [PATCH 04/12] chore: Move all static initializer code to PojoInliner --- .../TimefoldAotContribution.java | 24 +- .../boot/autoconfigure/util/PojoInliner.java | 131 ++++++---- .../autoconfigure/util/PojoInlinerTest.java | 236 +++++++++++------- 3 files changed, 239 insertions(+), 152 deletions(-) diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java index 2d35bfc762..43ce2306e5 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java @@ -1,7 +1,6 @@ package ai.timefold.solver.spring.boot.autoconfigure; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -34,14 +33,6 @@ public class TimefoldAotContribution implements BeanFactoryInitializationAotContribution { private static final String DEFAULT_SOLVER_CONFIG_NAME = "getSolverConfig"; - /** - * Name of the field that stores generated objects. - * Complex pojo's should consult this map before creating - * a new object to allow for cyclic references - * (i.e., a = new ArrayList(); a.add(a);). - */ - private static final String COMPLEX_POJO_MAP_FIELD_NAME = "$pojoMap"; - /** * Map of SolverConfigs that were recorded during the build. */ @@ -100,20 +91,15 @@ public void applyTo(GenerationContext generationContext, BeanFactoryInitializati // Create a generated class to hold all the solver configs GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("timefold-aot", builder -> { - PojoInliner pojoInliner = new PojoInliner(); - builder.addField(Map.class, "solverConfigMap", Modifier.STATIC); - builder.addField(Map.class, COMPLEX_POJO_MAP_FIELD_NAME, Modifier.STATIC); + final String SOLVER_CONFIG_MAP_FIELD = "solverConfigMap"; // Handwrite the SolverConfig map in the initializer - CodeBlock.Builder staticInitializer = CodeBlock.builder(); - staticInitializer.add("$L = new $T();", COMPLEX_POJO_MAP_FIELD_NAME, HashMap.class); - staticInitializer.add("\nsolverConfigMap = $L;", - pojoInliner.getInlinedPojo(solverConfigMap, staticInitializer)); - builder.addStaticBlock(staticInitializer.build()); + PojoInliner.inlineFields(builder, + PojoInliner.field(Map.class, SOLVER_CONFIG_MAP_FIELD, solverConfigMap)); // getSolverConfig fetches the SolverConfig with the given name from the map CodeBlock.Builder getSolverConfigMethod = CodeBlock.builder(); - getSolverConfigMethod.add("return ($T) solverConfigMap.get(name);", SolverConfig.class); + getSolverConfigMethod.add("return ($T) $L.get(name);", SolverConfig.class, SOLVER_CONFIG_MAP_FIELD); builder.addMethod(MethodSpec.methodBuilder("getSolverConfig") .addModifiers(Modifier.PUBLIC) .addModifiers(Modifier.STATIC) @@ -124,7 +110,7 @@ public void applyTo(GenerationContext generationContext, BeanFactoryInitializati // Returns the key set of the solver config map CodeBlock.Builder getSolverConfigNamesMethod = CodeBlock.builder(); - getSolverConfigNamesMethod.add("return new $T(solverConfigMap.keySet());", ArrayList.class); + getSolverConfigNamesMethod.add("return new $T($L.keySet());", ArrayList.class, SOLVER_CONFIG_MAP_FIELD); builder.addMethod(MethodSpec.methodBuilder("getSolverConfigNames") .addModifiers(Modifier.PUBLIC) .addModifiers(Modifier.STATIC) diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java index 3fd4663760..27226cd66b 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java @@ -12,6 +12,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -23,11 +24,13 @@ import org.apache.commons.text.StringEscapeUtils; import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.TypeSpec; public final class PojoInliner { static final String COMPLEX_POJO_MAP_FIELD_NAME = "$pojoMap"; private final Map complexPojoToIdentifier = new IdentityHashMap<>(); private final Set possibleCircularRecordReferenceSet = Collections.newSetFromMap(new IdentityHashMap<>()); + private final CodeBlock.Builder initializerBuilder; // The code below uses CodeBlock.Builder to generate the Java file // that stores the SolverConfig. @@ -38,17 +41,45 @@ public final class PojoInliner { // and surrounding it by double quotes. // - $T: Format as a fully qualified type, which allows you to use // classes without importing them. + PojoInliner() { + this.initializerBuilder = CodeBlock.builder(); + initializerBuilder.add("$T $L = new $T();", Map.class, COMPLEX_POJO_MAP_FIELD_NAME, HashMap.class); + } + + public record PojoField(Class type, String name, Object value) { + } + + public static PojoField field(Class type, String name, Object value) { + return new PojoField(type, name, value); + } + + public static void inlineFields(TypeSpec.Builder typeBuilder, PojoField... fields) { + PojoInliner inliner = new PojoInliner(); + for (PojoField field : fields) { + typeBuilder.addField(field.type(), field.name, + javax.lang.model.element.Modifier.PRIVATE, + javax.lang.model.element.Modifier.STATIC, + javax.lang.model.element.Modifier.FINAL); + } + for (PojoField field : fields) { + inliner.inlineField(field.name(), field.value()); + } + inliner.initializerBuilder.add("\n"); + typeBuilder.addStaticBlock(inliner.initializerBuilder.build()); + } + + void inlineField(String fieldName, Object fieldValue) { + initializerBuilder.add("\n$L = $L;", fieldName, getInlinedPojo(fieldValue)); + } /** * Serializes a Pojo to code that uses its no-args constructor * and setters to create the object. * * @param pojo The object to be serialized. - * @param initializerCode The code block builder of the initializer * @return A string that can be used in a {@link CodeBlock.Builder} to access the object */ - public String getInlinedPojo(Object pojo, - CodeBlock.Builder initializerCode) { + String getInlinedPojo(Object pojo) { // First, check for primitives if (pojo == null) { return "null"; @@ -111,7 +142,11 @@ public String getInlinedPojo(Object pojo, Enum pojoEnum = (Enum) pojo; return enumClass.getCanonicalName() + "." + pojoEnum.name(); } - return getInlinedComplexPojo(pojo, initializerCode); + return getInlinedComplexPojo(pojo); + } + + public CodeBlock.Builder getInitializerBuilder() { + return initializerBuilder; } /** @@ -130,7 +165,7 @@ private String getPojoFromMap(Object pojo) { /** * Serializes collections and complex POJOs to code */ - private String getInlinedComplexPojo(Object pojo, CodeBlock.Builder initializerCode) { + private String getInlinedComplexPojo(Object pojo) { if (possibleCircularRecordReferenceSet.contains(pojo)) { // Records do not have a no-args constructor, so we cannot safely serialize self-references in records // as we cannot do a map lookup before the record is created. @@ -146,7 +181,7 @@ private String getInlinedComplexPojo(Object pojo, CodeBlock.Builder initializerC if (pojo instanceof Record value) { // Records must set all fields at initialization time, // so we delay the declaration of its variable - return getInlinedRecord(value, initializerCode); + return getInlinedRecord(value); } // Object is not serialized yet // Create a new variable to store its value when setting its fields @@ -155,16 +190,16 @@ private String getInlinedComplexPojo(Object pojo, CodeBlock.Builder initializerC // First, check if it is a collection type if (pojo.getClass().isArray()) { - return getInlinedArray(newIdentifier, pojo, initializerCode); + return getInlinedArray(newIdentifier, pojo); } if (pojo instanceof List value) { - return getInlinedList(newIdentifier, value, initializerCode); + return getInlinedList(newIdentifier, value); } if (pojo instanceof Set value) { - return getInlinedSet(newIdentifier, value, initializerCode); + return getInlinedSet(newIdentifier, value); } if (pojo instanceof Map value) { - return getInlinedMap(newIdentifier, value, initializerCode); + return getInlinedMap(newIdentifier, value); } // Not a collection or record type, so serialize by creating a new instance and settings its fields @@ -172,7 +207,7 @@ private String getInlinedComplexPojo(Object pojo, CodeBlock.Builder initializerC throw new IllegalArgumentException("Cannot serialize (" + pojo + ") because its type (" + pojo.getClass() + ") is not public."); } - initializerCode.add("\n$T $L;", pojo.getClass(), newIdentifier); + initializerBuilder.add("\n$T $L;", pojo.getClass(), newIdentifier); try { Constructor constructor = pojo.getClass().getConstructor(); if (!Modifier.isPublic(constructor.getModifiers())) { @@ -183,83 +218,83 @@ private String getInlinedComplexPojo(Object pojo, CodeBlock.Builder initializerC throw new IllegalArgumentException("Cannot serialize (" + pojo + ") because its type (" + pojo.getClass() + ") does not have a public no-args constructor."); } - initializerCode.add("\n$L = new $T();", newIdentifier, pojo.getClass()); - initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); - inlineFieldsOfPojo(pojo.getClass(), newIdentifier, pojo, initializerCode); + initializerBuilder.add("\n$L = new $T();", newIdentifier, pojo.getClass()); + initializerBuilder.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + inlineFieldsOfPojo(pojo.getClass(), newIdentifier, pojo); return getPojoFromMap(pojo); } - private String getInlinedArray(String newIdentifier, Object array, CodeBlock.Builder initializerCode) { + private String getInlinedArray(String newIdentifier, Object array) { Class componentType = array.getClass().getComponentType(); if (!Modifier.isPublic(componentType.getModifiers())) { throw new IllegalArgumentException( "Cannot serialize array of type (" + componentType + ") because (" + componentType + ") is not public."); } - initializerCode.add("\n$T $L;", array.getClass(), newIdentifier); + initializerBuilder.add("\n$T $L;", array.getClass(), newIdentifier); // Get the length of the array int length = Array.getLength(array); // Create a new array from the component type with the given length - initializerCode.add("\n$L = new $T[$L];", newIdentifier, componentType, Integer.toString(length)); - initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + initializerBuilder.add("\n$L = new $T[$L];", newIdentifier, componentType, Integer.toString(length)); + initializerBuilder.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); for (int i = 0; i < length; i++) { // Set the elements of the array - initializerCode.add("\n$L[$L] = $L;", + initializerBuilder.add("\n$L[$L] = $L;", newIdentifier, Integer.toString(i), - getInlinedPojo(Array.get(array, i), initializerCode)); + getInlinedPojo(Array.get(array, i))); } return getPojoFromMap(array); } - private String getInlinedList(String newIdentifier, List list, CodeBlock.Builder initializerCode) { - initializerCode.add("\n$T $L;", List.class, newIdentifier); + private String getInlinedList(String newIdentifier, List list) { + initializerBuilder.add("\n$T $L;", List.class, newIdentifier); // Create an ArrayList - initializerCode.add("\n$L = new $T($L);", newIdentifier, ArrayList.class, Integer.toString(list.size())); - initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + initializerBuilder.add("\n$L = new $T($L);", newIdentifier, ArrayList.class, Integer.toString(list.size())); + initializerBuilder.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); for (Object item : list) { // Add each item of the list to the ArrayList - initializerCode.add("\n$L.add($L);", + initializerBuilder.add("\n$L.add($L);", newIdentifier, - getInlinedPojo(item, initializerCode)); + getInlinedPojo(item)); } return getPojoFromMap(list); } - private String getInlinedSet(String newIdentifier, Set set, CodeBlock.Builder initializerCode) { - initializerCode.add("\n$T $L;", Set.class, newIdentifier); + private String getInlinedSet(String newIdentifier, Set set) { + initializerBuilder.add("\n$T $L;", Set.class, newIdentifier); // Create a new HashSet - initializerCode.add("\n$L = new $T($L);", newIdentifier, LinkedHashSet.class, Integer.toString(set.size())); - initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + initializerBuilder.add("\n$L = new $T($L);", newIdentifier, LinkedHashSet.class, Integer.toString(set.size())); + initializerBuilder.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); for (Object item : set) { // Add each item of the set to the HashSet - initializerCode.add("\n$L.add($L);", + initializerBuilder.add("\n$L.add($L);", newIdentifier, - getInlinedPojo(item, initializerCode)); + getInlinedPojo(item)); } return getPojoFromMap(set); } - private String getInlinedMap(String newIdentifier, Map map, CodeBlock.Builder initializerCode) { - initializerCode.add("\n$T $L;", Map.class, newIdentifier); + private String getInlinedMap(String newIdentifier, Map map) { + initializerBuilder.add("\n$T $L;", Map.class, newIdentifier); // Create a HashMap - initializerCode.add("\n$L = new $T($L);", newIdentifier, LinkedHashMap.class, Integer.toString(map.size())); - initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + initializerBuilder.add("\n$L = new $T($L);", newIdentifier, LinkedHashMap.class, Integer.toString(map.size())); + initializerBuilder.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); for (Map.Entry entry : map.entrySet()) { // Put each entry of the map into the HashMap - initializerCode.add("\n$L.put($L, $L);", + initializerBuilder.add("\n$L.put($L, $L);", newIdentifier, - getInlinedPojo(entry.getKey(), initializerCode), - getInlinedPojo(entry.getValue(), initializerCode)); + getInlinedPojo(entry.getKey()), + getInlinedPojo(entry.getValue())); } return getPojoFromMap(map); } - private String getInlinedRecord(Record record, CodeBlock.Builder initializerCode) { + private String getInlinedRecord(Record record) { possibleCircularRecordReferenceSet.add(record); Class recordClass = record.getClass(); if (!Modifier.isPublic(recordClass.getModifiers())) { @@ -284,7 +319,7 @@ private String getInlinedRecord(Record record, CodeBlock.Builder initializerCode throw new IllegalStateException(e); } try { - componentAccessors[i] = getInlinedPojo(value, initializerCode); + componentAccessors[i] = getInlinedPojo(value); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Cannot serialize record (" + record + ") because the value (" + value + ") for its component (" + recordComponents[i].getName() + ") is not serializable.", e); @@ -301,8 +336,8 @@ private String getInlinedRecord(Record record, CodeBlock.Builder initializerCode } String newIdentifier = "$obj" + complexPojoToIdentifier.size(); complexPojoToIdentifier.put(record, newIdentifier); - initializerCode.add("\n$T $L = new $T($L);", recordClass, newIdentifier, recordClass, constructorArgs.toString()); - initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); + initializerBuilder.add("\n$T $L = new $T($L);", recordClass, newIdentifier, recordClass, constructorArgs.toString()); + initializerBuilder.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); return getPojoFromMap(record); } @@ -325,10 +360,8 @@ private Class getSerializedType(Class query) { * @param pojoClass A class assignable to pojo containing some of its fields. * @param identifier The name of the variable storing the serialized pojo. * @param pojo The object being serialized. - * @param initializerCode The {@link CodeBlock.Builder} to use to generate code in the initializer. */ - private void inlineFieldsOfPojo(Class pojoClass, String identifier, Object pojo, - CodeBlock.Builder initializerCode) { + private void inlineFieldsOfPojo(Class pojoClass, String identifier, Object pojo) { if (pojoClass == Object.class) { // We are the top-level, no more fields to set return; @@ -367,16 +400,16 @@ private void inlineFieldsOfPojo(Class pojoClass, String identifier, Object po try { // Convert the field value to code, and call the setter // corresponding to the field with the serialized field value. - initializerCode.add("\n$L.$L($L);", identifier, + initializerBuilder.add("\n$L.$L($L);", identifier, setterMethod.getName(), - getInlinedPojo(value, initializerCode)); + getInlinedPojo(value)); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Cannot serialize object (" + pojo + ") because the value (" + value + ") for its field (" + field.getName() + ") is not serializable.", e); } } try { - inlineFieldsOfPojo(pojoClass.getSuperclass(), identifier, pojo, initializerCode); + inlineFieldsOfPojo(pojoClass.getSuperclass(), identifier, pojo); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Cannot serialize type (" + pojoClass + ") because its superclass (" + pojoClass.getSuperclass() + ") is not serializable.", e); diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInlinerTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInlinerTest.java index dc90753c57..b750a1b5fb 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInlinerTest.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInlinerTest.java @@ -5,16 +5,21 @@ import static org.assertj.core.api.Assertions.assertThatCode; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import javax.lang.model.element.Modifier; + import ai.timefold.solver.core.config.solver.EnvironmentMode; import org.junit.jupiter.api.Test; import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.FieldSpec; +import org.springframework.javapoet.TypeSpec; public class PojoInlinerTest { private static class PrivatePojo { @@ -208,7 +213,15 @@ public void setAdditionalField(String additionalField) { } void assertBuilder(CodeBlock.Builder builder, String expected) { - assertThat(builder.build().toString().trim()).isEqualTo(expected.trim()); + if (expected.isEmpty()) { + assertThat(builder.build().toString().trim()) + .isEqualTo("%s %s = new %s();".formatted(Map.class.getCanonicalName(), COMPLEX_POJO_MAP_FIELD_NAME, + HashMap.class.getCanonicalName())); + } else { + assertThat(builder.build().toString().trim()) + .isEqualTo("%s %s = new %s();\n".formatted(Map.class.getCanonicalName(), COMPLEX_POJO_MAP_FIELD_NAME, + HashMap.class.getCanonicalName()) + expected.trim()); + } } private String getPojo(int id, Object value) { @@ -225,23 +238,22 @@ void assertAccessor(String accessor, int id, Object value) { @Test void inlinePrivateClasses() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); - assertThatCode(() -> inliner.getInlinedPojo(PrivatePojo.class, builder)) + assertThatCode(() -> inliner.getInlinedPojo(PrivatePojo.class)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Cannot serialize (" + PrivatePojo.class + ") because it is not a public class."); - assertThatCode(() -> inliner.getInlinedPojo(new PrivatePojo(), builder)) + assertThatCode(() -> inliner.getInlinedPojo(new PrivatePojo())) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Cannot serialize (" + new PrivatePojo() + ") because its type (" + PrivatePojo.class + ") is not public."); - assertThatCode(() -> inliner.getInlinedPojo(new PrivateRecord(), builder)) + assertThatCode(() -> inliner.getInlinedPojo(new PrivateRecord())) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Cannot serialize record (" + new PrivateRecord() + ") because its type (" + PrivateRecord.class + ") is not public."); - assertThatCode(() -> inliner.getInlinedPojo(PrivateEnum.VALUE, builder)) + assertThatCode(() -> inliner.getInlinedPojo(PrivateEnum.VALUE)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Cannot serialize (" + PrivateEnum.VALUE + ") because its type (" + PrivateEnum.class + ") is not a public class."); @@ -250,9 +262,8 @@ void inlinePrivateClasses() { @Test void inlinePrivateSetter() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); - assertThatCode(() -> inliner.getInlinedPojo(new PrivateSetterPojo(1, 2), builder)) + assertThatCode(() -> inliner.getInlinedPojo(new PrivateSetterPojo(1, 2))) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Cannot serialize type (" + PrivateSetterPojo.class + ") as it is missing a public setter method for field (bFieldWithSetter) of type (int)."); @@ -261,17 +272,17 @@ void inlinePrivateSetter() { @Test void inlineTypeUsingInterfaceImpl() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); + ArrayListPojo pojo = new ArrayListPojo(); pojo.setArrayList(new ArrayList<>()); - assertThatCode(() -> inliner.getInlinedPojo(pojo, builder)) + assertThatCode(() -> inliner.getInlinedPojo(pojo)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Cannot serialize type (" + ArrayListPojo.class + ") as its field (arrayList) uses an implementation of a collection (" + ArrayList.class + ") instead of the interface type (" + List.class + ")."); ArrayListRecord record = new ArrayListRecord(new ArrayList<>()); - assertThatCode(() -> inliner.getInlinedPojo(record, builder)) + assertThatCode(() -> inliner.getInlinedPojo(record)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Cannot serialize type (" + ArrayListRecord.class + ") as its component (arrayList) uses an implementation of a collection (" + ArrayList.class @@ -281,9 +292,9 @@ void inlineTypeUsingInterfaceImpl() { @Test void inlineNotPojo() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); + NotPojo pojo = new NotPojo(1, 2); - assertThatCode(() -> inliner.getInlinedPojo(pojo, builder)) + assertThatCode(() -> inliner.getInlinedPojo(pojo)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Cannot serialize type (" + NotPojo.class + ") as it is missing a public setter method for field (bFieldWithoutSetter) of type (int)."); @@ -292,9 +303,8 @@ void inlineNotPojo() { @Test void inlineExtendedNotPojo() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); - assertThatCode(() -> inliner.getInlinedPojo(new ExtendedNotPojo(), builder)) + assertThatCode(() -> inliner.getInlinedPojo(new ExtendedNotPojo())) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Cannot serialize type (" + ExtendedNotPojo.class + ") because its superclass (" + PrivateSetterPojo.class + ") is not serializable.") @@ -307,11 +317,10 @@ void inlineExtendedNotPojo() { @Test void inlineCircularRecord() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); LinkedRecordPojo pojo = LinkedRecordPojo.circular(); - assertThatCode(() -> inliner.getInlinedPojo(pojo, builder)) + assertThatCode(() -> inliner.getInlinedPojo(pojo)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Cannot serialize record (" + pojo + ") because the value (" + pojo.next + ") for its component (next) is not serializable.") @@ -328,65 +337,63 @@ void inlineCircularRecord() { @Test void inlinePrimitives() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); // null - assertThat(inliner.getInlinedPojo(null, builder)).isEqualTo("null"); - assertBuilder(builder, ""); + assertThat(inliner.getInlinedPojo(null)).isEqualTo("null"); + assertBuilder(inliner.getInitializerBuilder(), ""); // numbers - assertThat(inliner.getInlinedPojo(true, builder)).isEqualTo("true"); - assertBuilder(builder, ""); - assertThat(inliner.getInlinedPojo(false, builder)).isEqualTo("false"); - assertBuilder(builder, ""); - assertThat(inliner.getInlinedPojo((byte) 1, builder)).isEqualTo("((byte) 1)"); - assertBuilder(builder, ""); - assertThat(inliner.getInlinedPojo((short) 1, builder)).isEqualTo("((short) 1)"); - assertBuilder(builder, ""); - assertThat(inliner.getInlinedPojo(1, builder)).isEqualTo("1"); - assertBuilder(builder, ""); - assertThat(inliner.getInlinedPojo(1L, builder)).isEqualTo("1L"); - assertBuilder(builder, ""); - assertThat(inliner.getInlinedPojo(1f, builder)).isEqualTo("1.0f"); - assertBuilder(builder, ""); - assertThat(inliner.getInlinedPojo(1d, builder)).isEqualTo("1.0d"); - assertBuilder(builder, ""); + assertThat(inliner.getInlinedPojo(true)).isEqualTo("true"); + assertBuilder(inliner.getInitializerBuilder(), ""); + assertThat(inliner.getInlinedPojo(false)).isEqualTo("false"); + assertBuilder(inliner.getInitializerBuilder(), ""); + assertThat(inliner.getInlinedPojo((byte) 1)).isEqualTo("((byte) 1)"); + assertBuilder(inliner.getInitializerBuilder(), ""); + assertThat(inliner.getInlinedPojo((short) 1)).isEqualTo("((short) 1)"); + assertBuilder(inliner.getInitializerBuilder(), ""); + assertThat(inliner.getInlinedPojo(1)).isEqualTo("1"); + assertBuilder(inliner.getInitializerBuilder(), ""); + assertThat(inliner.getInlinedPojo(1L)).isEqualTo("1L"); + assertBuilder(inliner.getInitializerBuilder(), ""); + assertThat(inliner.getInlinedPojo(1f)).isEqualTo("1.0f"); + assertBuilder(inliner.getInitializerBuilder(), ""); + assertThat(inliner.getInlinedPojo(1d)).isEqualTo("1.0d"); + assertBuilder(inliner.getInitializerBuilder(), ""); // Strings and chars - assertThat(inliner.getInlinedPojo('a', builder)).isEqualTo("'\\u0061'"); - assertBuilder(builder, ""); - assertThat(inliner.getInlinedPojo("my\nmultiline\nstring", builder)).isEqualTo("\"my\\nmultiline\\nstring\""); - assertBuilder(builder, ""); + assertThat(inliner.getInlinedPojo('a')).isEqualTo("'\\u0061'"); + assertBuilder(inliner.getInitializerBuilder(), ""); + assertThat(inliner.getInlinedPojo("my\nmultiline\nstring")).isEqualTo("\"my\\nmultiline\\nstring\""); + assertBuilder(inliner.getInitializerBuilder(), ""); } @Test void inlineObjectPrimitives() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); // Classes - assertThat(inliner.getInlinedPojo(PojoInliner.class, builder)) + assertThat(inliner.getInlinedPojo(PojoInliner.class)) .isEqualTo(PojoInliner.class.getCanonicalName() + ".class"); - assertBuilder(builder, ""); + assertBuilder(inliner.getInitializerBuilder(), ""); // Enums - assertThat(inliner.getInlinedPojo(EnvironmentMode.REPRODUCIBLE, builder)) + assertThat(inliner.getInlinedPojo(EnvironmentMode.REPRODUCIBLE)) .isEqualTo(EnvironmentMode.class.getCanonicalName() + "." + EnvironmentMode.REPRODUCIBLE.name()); - assertBuilder(builder, ""); + assertBuilder(inliner.getInitializerBuilder(), ""); // ClassLoader - assertThat(inliner.getInlinedPojo(Thread.currentThread().getContextClassLoader(), builder)) + assertThat(inliner.getInlinedPojo(Thread.currentThread().getContextClassLoader())) .isEqualTo("Thread.currentThread().getContextClassLoader()"); - assertBuilder(builder, ""); + assertBuilder(inliner.getInitializerBuilder(), ""); } @Test void inlinePrimitiveArray() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); + int[] pojo = new int[] { 1, 2, 3, 4, 5 }; - String accessor = inliner.getInlinedPojo(pojo, builder); - assertBuilder(builder, + String accessor = inliner.getInlinedPojo(pojo); + assertBuilder(inliner.getInitializerBuilder(), """ int[] $obj0; $obj0 = new int[5]; @@ -403,13 +410,13 @@ void inlinePrimitiveArray() { @Test void inlineObjectArray() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); + Object[] pojo = new Object[3]; pojo[0] = null; pojo[1] = pojo; pojo[2] = "Item"; - String accessor = inliner.getInlinedPojo(pojo, builder); - assertBuilder(builder, + String accessor = inliner.getInlinedPojo(pojo); + assertBuilder(inliner.getInitializerBuilder(), """ java.lang.Object[] $obj0; $obj0 = new java.lang.Object[3]; @@ -424,10 +431,10 @@ void inlineObjectArray() { @Test void inlineIntList() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); + List pojo = List.of(1, 2, 3); - String accessor = inliner.getInlinedPojo(pojo, builder); - assertBuilder(builder, + String accessor = inliner.getInlinedPojo(pojo); + assertBuilder(inliner.getInitializerBuilder(), """ java.util.List $obj0; $obj0 = new java.util.ArrayList(3); @@ -442,13 +449,13 @@ void inlineIntList() { @Test void inlineIntSet() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); + Set pojo = new LinkedHashSet<>(3); pojo.add(1); pojo.add(2); pojo.add(3); - String accessor = inliner.getInlinedPojo(pojo, builder); - assertBuilder(builder, + String accessor = inliner.getInlinedPojo(pojo); + assertBuilder(inliner.getInitializerBuilder(), """ java.util.Set $obj0; $obj0 = new java.util.LinkedHashSet(3); @@ -463,13 +470,13 @@ void inlineIntSet() { @Test void inlineIntStringMap() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); + Map pojo = new LinkedHashMap<>(3); pojo.put(1, "a"); pojo.put(2, "b"); pojo.put(3, "c"); - String accessor = inliner.getInlinedPojo(pojo, builder); - assertBuilder(builder, + String accessor = inliner.getInlinedPojo(pojo); + assertBuilder(inliner.getInitializerBuilder(), """ java.util.Map $obj0; $obj0 = new java.util.LinkedHashMap(3); @@ -484,9 +491,9 @@ void inlineIntStringMap() { @Test void inlinePojo() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); + BasicPojo pojo = new BasicPojo(new BasicPojo(null, 0, "parent"), 1, "child"); - String accessor = inliner.getInlinedPojo(pojo, builder); + String accessor = inliner.getInlinedPojo(pojo); String expected = """ %s $obj0; $obj0 = new %s(); @@ -505,23 +512,23 @@ void inlinePojo() { BasicPojo.class.getCanonicalName(), BasicPojo.class.getCanonicalName(), getPojo(1, pojo.getParentPojo())); - assertBuilder(builder, expected); + assertBuilder(inliner.getInitializerBuilder(), expected); assertAccessor(accessor, 0, pojo); - String parentAccessor = inliner.getInlinedPojo(pojo.getParentPojo(), builder); - assertBuilder(builder, expected); + String parentAccessor = inliner.getInlinedPojo(pojo.getParentPojo()); + assertBuilder(inliner.getInitializerBuilder(), expected); assertAccessor(parentAccessor, 1, pojo.getParentPojo()); } @Test void inlineExtendedPojo() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); + ExtendedPojo pojo = new ExtendedPojo(); pojo.setAdditionalField("newField"); pojo.setId(1); pojo.setName("child"); pojo.setParentPojo(new BasicPojo(null, 0, "parent")); - String accessor = inliner.getInlinedPojo(pojo, builder); + String accessor = inliner.getInlinedPojo(pojo); String expected = """ %s $obj0; $obj0 = new %s(); @@ -541,20 +548,20 @@ void inlineExtendedPojo() { BasicPojo.class.getCanonicalName(), BasicPojo.class.getCanonicalName(), getPojo(1, pojo.getParentPojo())); - assertBuilder(builder, expected); + assertBuilder(inliner.getInitializerBuilder(), expected); assertAccessor(accessor, 0, pojo); - String parentAccessor = inliner.getInlinedPojo(pojo.getParentPojo(), builder); - assertBuilder(builder, expected); + String parentAccessor = inliner.getInlinedPojo(pojo.getParentPojo()); + assertBuilder(inliner.getInitializerBuilder(), expected); assertAccessor(parentAccessor, 1, pojo.getParentPojo()); } @Test void inlineRecord() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); + BasicPojo pojo = new BasicPojo(null, 0, "name"); RecordPojo recordPojo = new RecordPojo("INSERT", 0, pojo); - String accessor = inliner.getInlinedPojo(recordPojo, builder); + String accessor = inliner.getInlinedPojo(recordPojo); String expected = """ %s $obj0; $obj0 = new %s(); @@ -569,17 +576,17 @@ void inlineRecord() { RecordPojo.class.getCanonicalName(), RecordPojo.class.getCanonicalName(), getPojo(0, recordPojo.pojo())); - assertBuilder(builder, expected); + assertBuilder(inliner.getInitializerBuilder(), expected); assertAccessor(accessor, 1, recordPojo); - String partAccessor = inliner.getInlinedPojo(recordPojo.pojo(), builder); - assertBuilder(builder, expected); + String partAccessor = inliner.getInlinedPojo(recordPojo.pojo()); + assertBuilder(inliner.getInitializerBuilder(), expected); assertAccessor(partAccessor, 0, recordPojo.pojo()); } @Test void inlineNonCircularRecord() { PojoInliner inliner = new PojoInliner(); - CodeBlock.Builder builder = CodeBlock.builder(); + LinkedRecordPojo pojo = LinkedRecordPojo.nonCircular(); String expected = """ %s $obj0; @@ -598,14 +605,75 @@ void inlineNonCircularRecord() { LinkedRecordPojo.class.getCanonicalName(), LinkedRecordPojo.class.getCanonicalName(), getPojo(0, pojo.next())); - String accessor = inliner.getInlinedPojo(pojo, builder); - assertBuilder(builder, expected); + String accessor = inliner.getInlinedPojo(pojo); + assertBuilder(inliner.getInitializerBuilder(), expected); assertAccessor(accessor, 2, pojo); - String nextAccessor = inliner.getInlinedPojo(pojo.next(), builder); - assertBuilder(builder, expected); + String nextAccessor = inliner.getInlinedPojo(pojo.next()); + assertBuilder(inliner.getInitializerBuilder(), expected); assertAccessor(nextAccessor, 0, pojo.next()); - String referenceAccessor = inliner.getInlinedPojo(pojo.next().getReference(), builder); - assertBuilder(builder, expected); + String referenceAccessor = inliner.getInlinedPojo(pojo.next().getReference()); + assertBuilder(inliner.getInitializerBuilder(), expected); assertAccessor(referenceAccessor, 1, pojo.next().getReference()); } + + @Test + void inlineFieldToStaticBlock() { + PojoInliner inliner = new PojoInliner(); + inliner.inlineField("myField", new BasicPojo(null, 0, "name")); + String expected = """ + %s $obj0; + $obj0 = new %s(); + $pojoMap.put("$obj0", $obj0); + $obj0.setId(0); + $obj0.setName("name"); + $obj0.setParentPojo(null); + myField = %s; + """.formatted(BasicPojo.class.getCanonicalName(), + BasicPojo.class.getCanonicalName(), + getPojo(0, new BasicPojo(null, 0, "name"))); + assertBuilder(inliner.getInitializerBuilder(), expected); + } + + @Test + void inlineMultipleFieldsToStaticBlock() { + var typeBuilder = TypeSpec.classBuilder("TestClass"); + PojoInliner.inlineFields(typeBuilder, + PojoInliner.field(int.class, "a", 1), + PojoInliner.field(BasicPojo.class, "b", new BasicPojo(null, 0, "name")), + PojoInliner.field(Object.class, "c", "text")); + TypeSpec typeSpec = typeBuilder.build(); + assertThat(typeSpec.fieldSpecs).containsExactly( + FieldSpec.builder(int.class, "a", Modifier.PRIVATE, + Modifier.STATIC, + Modifier.FINAL) + .build(), + FieldSpec.builder(BasicPojo.class, "b", Modifier.PRIVATE, + Modifier.STATIC, + Modifier.FINAL) + .build(), + FieldSpec.builder(Object.class, "c", Modifier.PRIVATE, + Modifier.STATIC, + Modifier.FINAL) + .build()); + assertThat(typeSpec.staticBlock.toString()).isEqualTo( + """ + static { + %s %s = new %s(); + a = 1; + %s $obj0; + $obj0 = new %s(); + $pojoMap.put("$obj0", $obj0); + $obj0.setId(0); + $obj0.setName("name"); + $obj0.setParentPojo(null); + b = %s; + c = "text"; + } + """.formatted(Map.class.getCanonicalName(), + COMPLEX_POJO_MAP_FIELD_NAME, + HashMap.class.getCanonicalName(), + BasicPojo.class.getCanonicalName(), + BasicPojo.class.getCanonicalName(), + getPojo(0, new BasicPojo(null, 0, "name")))); + } } From 9a381877ff770566e6d5d949288c1d27ba0eae24 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 6 Feb 2024 13:12:22 -0500 Subject: [PATCH 05/12] ci: Install GraalVM native image in native workflow This is to allow spring-integration-test to run in native mode, as it requires a local GraalVM native-image installation. --- .github/workflows/pull_request.yml | 9 +++++++++ 1 file changed, 9 insertions(+) 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' From 20710a0a122660c7fffb699b9a8580ce4f60b315 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 6 Feb 2024 14:40:45 -0500 Subject: [PATCH 06/12] chore: Rename Timefold -> TimefoldSolver where it makes sense Also changed the UnsupportedConstraintVerifier class from an anonymous class to an inner class. --- .../TimefoldBenchmarkAutoConfiguration.java | 4 +- ...ava => TimefoldSolverAotContribution.java} | 4 +- ...a => TimefoldSolverAutoConfiguration.java} | 8 +-- ...ry.java => TimefoldSolverBeanFactory.java} | 55 +++++++++++-------- ...ot.autoconfigure.AutoConfiguration.imports | 4 +- ... TimefoldSolverAutoConfigurationTest.java} | 24 +++++--- ...rMultipleSolverAutoConfigurationTest.java} | 24 +++++--- 7 files changed, 72 insertions(+), 51 deletions(-) rename spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/{TimefoldAotContribution.java => TimefoldSolverAotContribution.java} (98%) rename spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/{TimefoldAutoConfiguration.java => TimefoldSolverAutoConfiguration.java} (99%) rename spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/{TimefoldBeanFactory.java => TimefoldSolverBeanFactory.java} (83%) rename spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/{TimefoldAutoConfigurationTest.java => TimefoldSolverAutoConfigurationTest.java} (97%) rename spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/{TimefoldMultipleSolverAutoConfigurationTest.java => TimefoldSolverMultipleSolverAutoConfigurationTest.java} (97%) 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/TimefoldAotContribution.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java similarity index 98% rename from spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java rename to spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java index 43ce2306e5..9751627560 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java @@ -30,7 +30,7 @@ import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; -public class TimefoldAotContribution implements BeanFactoryInitializationAotContribution { +public class TimefoldSolverAotContribution implements BeanFactoryInitializationAotContribution { private static final String DEFAULT_SOLVER_CONFIG_NAME = "getSolverConfig"; /** @@ -38,7 +38,7 @@ public class TimefoldAotContribution implements BeanFactoryInitializationAotCont */ private final Map solverConfigMap; - public TimefoldAotContribution(Map solverConfigMap) { + public TimefoldSolverAotContribution(Map solverConfigMap) { this.solverConfigMap = solverConfigMap; } 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 99% 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 ce7ec6d99c..9681ada1bb 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 @@ -66,11 +66,11 @@ @ConditionalOnMissingBean({ SolverConfig.class, SolverFactory.class, ScoreManager.class, SolutionManager.class, SolverManager.class }) @EnableConfigurationProperties({ TimefoldProperties.class }) -public class TimefoldAutoConfiguration +public class TimefoldSolverAutoConfiguration implements BeanClassLoaderAware, ApplicationContextAware, EnvironmentAware, BeanFactoryInitializationAotProcessor, BeanFactoryPostProcessor { - 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[] PLANNING_ENTITY_FIELD_ANNOTATIONS = new Class[] { PlanningPin.class, @@ -89,7 +89,7 @@ public class TimefoldAutoConfiguration private ClassLoader beanClassLoader; private TimefoldProperties timefoldProperties; - protected TimefoldAutoConfiguration() { + protected TimefoldSolverAutoConfiguration() { } @Override @@ -147,7 +147,7 @@ private Map getSolverConfigMap() { @Override public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { Map solverConfigMap = getSolverConfigMap(); - return new TimefoldAotContribution(solverConfigMap); + return new TimefoldSolverAotContribution(solverConfigMap); } @Override diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldBeanFactory.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverBeanFactory.java similarity index 83% rename from spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldBeanFactory.java rename to spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverBeanFactory.java index 6f09e78321..ec05bf8695 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldBeanFactory.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverBeanFactory.java @@ -42,13 +42,13 @@ import com.fasterxml.jackson.databind.Module; /** - * Must be seperated from {@link TimefoldAutoConfiguration} since - * {@link TimefoldAutoConfiguration} will not be available at runtime + * 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 TimefoldBeanFactory implements ApplicationContextAware, EnvironmentAware { +public class TimefoldSolverBeanFactory implements ApplicationContextAware, EnvironmentAware { private ApplicationContext context; private TimefoldProperties timefoldProperties; @@ -133,7 +133,7 @@ public > SolutionManager + 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") @@ -162,28 +188,11 @@ ConstraintVerifier constraintVerifier() { 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) + // 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 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 new UnsupportedConstraintVerifier<>(noConstraintProviderErrorMsg); } return ConstraintVerifier.create(solverConfig); 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 d64461ec84..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,3 +1,3 @@ -ai.timefold.solver.spring.boot.autoconfigure.TimefoldAutoConfiguration +ai.timefold.solver.spring.boot.autoconfigure.TimefoldSolverAutoConfiguration ai.timefold.solver.spring.boot.autoconfigure.TimefoldBenchmarkAutoConfiguration -ai.timefold.solver.spring.boot.autoconfigure.TimefoldBeanFactory \ No newline at end of file +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 97% 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 a08228ce8e..ccaab0848e 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 @@ -68,7 +68,7 @@ import org.springframework.test.context.TestExecutionListeners; @TestExecutionListeners -class TimefoldAutoConfigurationTest { +class TimefoldSolverAutoConfigurationTest { private final ApplicationContextRunner contextRunner; private final ApplicationContextRunner emptyContextRunner; @@ -81,27 +81,32 @@ class TimefoldAutoConfigurationTest { private final FilteredClassLoader testFilteredClassLoader; private final FilteredClassLoader noGizmoFilteredClassLoader; - public TimefoldAutoConfigurationTest() { + public TimefoldSolverAutoConfigurationTest() { contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) + .withConfiguration( + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) .withUserConfiguration(NormalSpringTestConfiguration.class); emptyContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) + .withConfiguration( + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) .withUserConfiguration(EmptySpringTestConfiguration.class); benchmarkContextRunner = new ApplicationContextRunner() .withConfiguration( - AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class, + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class, TimefoldBenchmarkAutoConfiguration.class)) .withUserConfiguration(NormalSpringTestConfiguration.class); gizmoContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) + .withConfiguration( + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) .withUserConfiguration(GizmoSpringTestConfiguration.class); chainedContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) + .withConfiguration( + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) .withUserConfiguration(ChainedSpringTestConfiguration.class); multimoduleRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) + .withConfiguration( + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) .withUserConfiguration(MultiModuleSpringTestConfiguration.class); allDefaultsFilteredClassLoader = new FilteredClassLoader(FilteredClassLoader.PackageFilter.of("ai.timefold.solver.test"), @@ -113,7 +118,8 @@ public TimefoldAutoConfigurationTest() { FilteredClassLoader.ClassPathResourceFilter.of( new ClassPathResource(TimefoldProperties.DEFAULT_SOLVER_CONFIG_URL))); noUserConfigurationContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)); + .withConfiguration( + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)); } @Test 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 97% 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 ec8f3ad311..1777f87bb9 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,33 +67,39 @@ class TimefoldMultipleSolverAutoConfigurationTest { private final ApplicationContextRunner multimoduleRunner; private final FilteredClassLoader allDefaultsFilteredClassLoader; - public TimefoldMultipleSolverAutoConfigurationTest() { + public TimefoldSolverMultipleSolverAutoConfigurationTest() { contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) + .withConfiguration( + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) .withUserConfiguration(NormalSpringTestConfiguration.class); emptyContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) + .withConfiguration( + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) .withUserConfiguration(EmptySpringTestConfiguration.class); benchmarkContextRunner = new ApplicationContextRunner() .withConfiguration( - AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class, + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class, TimefoldBenchmarkAutoConfiguration.class)) .withUserConfiguration(NormalSpringTestConfiguration.class); gizmoContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) + .withConfiguration( + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) .withUserConfiguration(GizmoSpringTestConfiguration.class); chainedContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.class)) + .withConfiguration( + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) .withUserConfiguration(ChainedSpringTestConfiguration.class); multimoduleRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TimefoldAutoConfiguration.class, TimefoldBeanFactory.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, TimefoldBeanFactory.class)); + .withConfiguration( + AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)); } @Test From 6e87d46edeb60fe1181a5f6368723169f59f412f Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 6 Feb 2024 14:50:01 -0500 Subject: [PATCH 07/12] chore: Rename Timefold -> TimefoldSolver in spring-boot-integration-test --- ...{TimefoldController.java => TimefoldSolverController.java} | 4 ++-- ...oldSpringBootApp.java => TimefoldSolverSpringBootApp.java} | 4 ++-- ...st.java => TimefoldSolverTestResourceIntegrationTest.java} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/{TimefoldController.java => TimefoldSolverController.java} (87%) rename spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/{TimefoldSpringBootApp.java => TimefoldSolverSpringBootApp.java} (67%) rename spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/{TimefoldTestResourceIntegrationTest.java => TimefoldSolverTestResourceIntegrationTest.java} (97%) diff --git a/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldController.java b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSolverController.java similarity index 87% rename from spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldController.java rename to spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSolverController.java index 5bcab06c59..95d80ee0d5 100644 --- a/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldController.java +++ b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSolverController.java @@ -11,10 +11,10 @@ @RestController @RequestMapping("/integration-test") -public class TimefoldController { +public class TimefoldSolverController { private final SolverFactory solverFactory; - public TimefoldController(SolverFactory solverFactory) { + public TimefoldSolverController(SolverFactory solverFactory) { this.solverFactory = solverFactory; } diff --git a/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSpringBootApp.java b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSolverSpringBootApp.java similarity index 67% rename from spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSpringBootApp.java rename to spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSolverSpringBootApp.java index fa9b392aef..bc26a7a1eb 100644 --- a/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSpringBootApp.java +++ b/spring-integration/spring-boot-integration-test/src/main/java/ai/timefold/solver/spring/boot/it/TimefoldSolverSpringBootApp.java @@ -4,9 +4,9 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class TimefoldSpringBootApp { +public class TimefoldSolverSpringBootApp { public static void main(String[] args) { - SpringApplication.run(TimefoldSpringBootApp.class, args); + SpringApplication.run(TimefoldSolverSpringBootApp.class, args); } } diff --git a/spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/TimefoldTestResourceIntegrationTest.java b/spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/TimefoldSolverTestResourceIntegrationTest.java similarity index 97% rename from spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/TimefoldTestResourceIntegrationTest.java rename to spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/TimefoldSolverTestResourceIntegrationTest.java index 5dc165b132..991f322b2b 100644 --- a/spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/TimefoldTestResourceIntegrationTest.java +++ b/spring-integration/spring-boot-integration-test/src/test/java/ai/timefold/solver/spring/boot/it/TimefoldSolverTestResourceIntegrationTest.java @@ -12,7 +12,7 @@ import org.springframework.test.web.reactive.server.WebTestClient; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class TimefoldTestResourceIntegrationTest { +public class TimefoldSolverTestResourceIntegrationTest { @LocalServerPort String port; From d170f9af2f664e46576f6de243b43e518879ae60 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 8 Feb 2024 17:42:21 -0500 Subject: [PATCH 08/12] chore: Use ObjectMapper instead of PojoInliner to transfer SolverConfig to generated method in spring Finding out why ObjectMapper wasn't before was trickly. The short of it is that java.lang.Class was registered for reflection, which made GraalVM think its cachedHashCode field was reachable in an initializer, which is not allowed. So we needed to make registerTypeRecursively ignore java.lang.Class and java.lang.ClassLoader. Then ObjectMapper is happy. I would use SolverConfigIO/JAXB, but JAXB accesses itself using reflection, so I would need to register a bunch more (possible unstable) classes for reflections to use it. --- .../spring-boot-autoconfigure/pom.xml | 19 +- .../TimefoldSolverAotContribution.java | 212 +++--- .../TimefoldSolverAutoConfiguration.java | 25 +- .../util/JacksonCustomPhaseConfigMixin.java | 12 + .../util/JacksonSolverConfigMixin.java | 19 + .../util/JacksonTerminationConfigMixin.java | 8 + .../boot/autoconfigure/util/PojoInliner.java | 419 ----------- .../autoconfigure/util/PojoInlinerTest.java | 679 ------------------ 8 files changed, 151 insertions(+), 1242 deletions(-) create mode 100644 spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonCustomPhaseConfigMixin.java create mode 100644 spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonSolverConfigMixin.java create mode 100644 spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonTerminationConfigMixin.java delete mode 100644 spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java delete mode 100644 spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInlinerTest.java diff --git a/spring-integration/spring-boot-autoconfigure/pom.xml b/spring-integration/spring-boot-autoconfigure/pom.xml index af503ff839..71b31dad27 100644 --- a/spring-integration/spring-boot-autoconfigure/pom.xml +++ b/spring-integration/spring-boot-autoconfigure/pom.xml @@ -62,9 +62,19 @@ org.springframework.boot spring-boot-autoconfigure + + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-core + - org.apache.commons - commons-text + com.fasterxml.jackson.core + jackson-databind @@ -82,11 +92,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/TimefoldSolverAotContribution.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java index 9751627560..2903037df0 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java @@ -1,23 +1,24 @@ package ai.timefold.solver.spring.boot.autoconfigure; -import java.util.ArrayList; +import java.lang.reflect.Field; import java.util.HashSet; -import java.util.List; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; -import javax.lang.model.element.Modifier; - import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.api.solver.SolverManager; +import ai.timefold.solver.core.config.phase.custom.CustomPhaseConfig; 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.spring.boot.autoconfigure.config.SolverManagerProperties; import ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties; -import ai.timefold.solver.spring.boot.autoconfigure.util.PojoInliner; +import ai.timefold.solver.spring.boot.autoconfigure.util.JacksonCustomPhaseConfigMixin; +import ai.timefold.solver.spring.boot.autoconfigure.util.JacksonSolverConfigMixin; +import ai.timefold.solver.spring.boot.autoconfigure.util.JacksonTerminationConfigMixin; -import org.springframework.aot.generate.DefaultMethodReference; -import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedMethod; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; @@ -28,7 +29,9 @@ import org.springframework.boot.context.properties.bind.Binder; import org.springframework.core.env.Environment; import org.springframework.javapoet.CodeBlock; -import org.springframework.javapoet.MethodSpec; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; public class TimefoldSolverAotContribution implements BeanFactoryInitializationAotContribution { private static final String DEFAULT_SOLVER_CONFIG_NAME = "getSolverConfig"; @@ -37,6 +40,9 @@ public class TimefoldSolverAotContribution implements BeanFactoryInitializationA * Map of SolverConfigs that were recorded during the build. */ private final Map solverConfigMap; + private final static Set> BANNED_CLASSES = Set.of( + Class.class, + ClassLoader.class); public TimefoldSolverAotContribution(Map solverConfigMap) { this.solverConfigMap = solverConfigMap; @@ -60,6 +66,46 @@ private void registerType(ReflectionHints reflectionHints, Class type) { MemberCategory.INVOKE_PUBLIC_METHODS); } + private void registerTypeRecursively(ReflectionHints reflectionHints, Class type, Set> visited) { + if (type == null || BANNED_CLASSES.contains(type) || visited.contains(type)) { + return; + } + visited.add(type); + registerType(reflectionHints, type); + for (Field field : type.getDeclaredFields()) { + registerTypeRecursively(reflectionHints, field.getType(), visited); + } + registerTypeRecursively(reflectionHints, type.getSuperclass(), visited); + } + + public static void registerSolverConfigs(Environment environment, + ConfigurableListableBeanFactory beanFactory, + ClassLoader classLoader, + Map solverConfigMap) { + BindResult result = Binder.get(environment).bind("timefold", TimefoldProperties.class); + TimefoldProperties timefoldProperties = result.orElseGet(TimefoldProperties::new); + if (solverConfigMap.isEmpty()) { + beanFactory.registerSingleton(DEFAULT_SOLVER_CONFIG_NAME, new SolverConfig(classLoader)); + return; + } + + if (timefoldProperties.getSolver() == null || timefoldProperties.getSolver().size() == 1) { + beanFactory.registerSingleton(DEFAULT_SOLVER_CONFIG_NAME, solverConfigMap.values().iterator().next()); + } 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)); + }); + } + } + // The code below uses CodeBlock.Builder to generate the Java file // that stores the SolverConfig. // CodeBlock.Builder.add supports different kinds of formatting args. @@ -76,125 +122,61 @@ public void applyTo(GenerationContext generationContext, BeanFactoryInitializati // Register all classes reachable from the SolverConfig for reflection // (so we can read their metadata) Set> classSet = new HashSet<>(); - for (SolverConfig solverConfig : solverConfigMap.values()) { - solverConfig.visitReferencedClasses(clazz -> { + Map solverXmlMap = new LinkedHashMap<>(); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.addMixIn(SolverConfig.class, JacksonSolverConfigMixin.class); + objectMapper.addMixIn(TerminationConfig.class, JacksonTerminationConfigMixin.class); + objectMapper.addMixIn(CustomPhaseConfig.class, JacksonCustomPhaseConfigMixin.class); + for (Map.Entry solverConfigEntry : solverConfigMap.entrySet()) { + solverConfigEntry.getValue().visitReferencedClasses(clazz -> { if (clazz != null) { classSet.add(clazz); } }); + try { + solverXmlMap.put(solverConfigEntry.getKey(), objectMapper.writeValueAsString(solverConfigEntry.getValue())); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } + registerType(reflectionHints, SolverConfig.class); + registerTypeRecursively(reflectionHints, SolverConfig.class, new HashSet<>()); for (Class clazz : classSet) { registerType(reflectionHints, clazz); } // Create a generated class to hold all the solver configs - GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("timefold-aot", + GeneratedMethod generatedMethod = beanFactoryInitializationCode.getMethods().add("getSolverConfigs", builder -> { - final String SOLVER_CONFIG_MAP_FIELD = "solverConfigMap"; - - // Handwrite the SolverConfig map in the initializer - PojoInliner.inlineFields(builder, - PojoInliner.field(Map.class, SOLVER_CONFIG_MAP_FIELD, solverConfigMap)); - - // getSolverConfig fetches the SolverConfig with the given name from the map - CodeBlock.Builder getSolverConfigMethod = CodeBlock.builder(); - getSolverConfigMethod.add("return ($T) $L.get(name);", SolverConfig.class, SOLVER_CONFIG_MAP_FIELD); - builder.addMethod(MethodSpec.methodBuilder("getSolverConfig") - .addModifiers(Modifier.PUBLIC) - .addModifiers(Modifier.STATIC) - .addParameter(String.class, "name") - .returns(SolverConfig.class) - .addCode(getSolverConfigMethod.build()) - .build()); - - // Returns the key set of the solver config map - CodeBlock.Builder getSolverConfigNamesMethod = CodeBlock.builder(); - getSolverConfigNamesMethod.add("return new $T($L.keySet());", ArrayList.class, SOLVER_CONFIG_MAP_FIELD); - builder.addMethod(MethodSpec.methodBuilder("getSolverConfigNames") - .addModifiers(Modifier.PUBLIC) - .addModifiers(Modifier.STATIC) - .returns(List.class) - .addCode(getSolverConfigNamesMethod.build()) - .build()); - - // Registers the SolverConfig(s) as beans that can be injected - CodeBlock.Builder registerSolverConfigsMethod = CodeBlock.builder(); - // Get the timefold properties from the environment - registerSolverConfigsMethod.add("$T timefoldPropertiesResult = " + - "$T.get(environment).bind(\"timefold\", $T.class);", BindResult.class, Binder.class, - TimefoldProperties.class); - registerSolverConfigsMethod.add("\n$T timefoldProperties = timefoldPropertiesResult.orElseGet($T::new);", - TimefoldProperties.class, TimefoldProperties.class); - - // Get the names of the solverConfigs - registerSolverConfigsMethod.add("\n$T solverConfigNames = getSolverConfigNames();\n", List.class); - - // If there are no solverConfigs... - registerSolverConfigsMethod.beginControlFlow("if (solverConfigNames.isEmpty())"); - // Create an empty one that can be used for injection - registerSolverConfigsMethod.add( - "\nbeanFactory.registerSingleton($S, new $T(beanFactory.getBeanClassLoader()));", - DEFAULT_SOLVER_CONFIG_NAME, SolverConfig.class); - registerSolverConfigsMethod.add("return;\n"); - registerSolverConfigsMethod.endControlFlow(); - - // If there is only a single solver - registerSolverConfigsMethod.beginControlFlow( - "if (timefoldProperties.getSolver() == null || timefoldProperties.getSolver().size() == 1)"); - // Use the default solver config name - registerSolverConfigsMethod.add( - "\nbeanFactory.registerSingleton($S, getSolverConfig((String) solverConfigNames.get(0)));", - DEFAULT_SOLVER_CONFIG_NAME); - registerSolverConfigsMethod.add("return;\n"); - registerSolverConfigsMethod.endControlFlow(); - - // Otherwise, for each solver... - registerSolverConfigsMethod.beginControlFlow("for (Object solverNameObj : solverConfigNames)"); - // Get the solver config with the given name - registerSolverConfigsMethod.add("\nString solverName = (String) solverNameObj;"); - registerSolverConfigsMethod.add( - "\n$T solverConfig = getSolverConfig(solverName);", - SolverConfig.class); - - // Create a solver manager from that solver config - registerSolverConfigsMethod.add("\n$T solverFactory = $T.create(solverConfig);", SolverFactory.class, - SolverFactory.class); - registerSolverConfigsMethod.add("\n$T solverManagerConfig = new $T();", SolverManagerConfig.class, - SolverManagerConfig.class); - registerSolverConfigsMethod.add("\n$T solverManagerProperties = timefoldProperties.getSolverManager();\n", - SolverManagerProperties.class); - registerSolverConfigsMethod.beginControlFlow( - "if (solverManagerProperties != null && solverManagerProperties.getParallelSolverCount() != null)"); - registerSolverConfigsMethod.add( - "\nsolverManagerConfig.setParallelSolverCount(solverManagerProperties.getParallelSolverCount());\n"); - registerSolverConfigsMethod.endControlFlow(); - - // Register that solver manager - registerSolverConfigsMethod.add( - "\nbeanFactory.registerSingleton(solverName, $T.create(solverFactory, solverManagerConfig));\n", - SolverManager.class); - registerSolverConfigsMethod.endControlFlow(); - - builder.addMethod(MethodSpec.methodBuilder("registerSolverConfigs") - .addModifiers(Modifier.PUBLIC) - .addModifiers(Modifier.STATIC) - .addParameter(Environment.class, "environment") - .addParameter(ConfigurableListableBeanFactory.class, "beanFactory") - .addCode(registerSolverConfigsMethod.build()) - .build()); - - builder.build(); + builder.addParameter(Environment.class, "environment"); + builder.addParameter(ConfigurableListableBeanFactory.class, "beanFactory"); + var code = CodeBlock.builder(); + code.beginControlFlow("try"); + code.add("$T<$T, $T> solverConfigMap = new $T<>();\n", Map.class, String.class, SolverConfig.class, + LinkedHashMap.class); + code.add("$T objectMapper = new $T();\n", ObjectMapper.class, ObjectMapper.class); + code.add("objectMapper.addMixIn($T.class, $T.class);\n", SolverConfig.class, + JacksonSolverConfigMixin.class); + code.add("objectMapper.addMixIn($T.class, $T.class);\n", TerminationConfig.class, + JacksonTerminationConfigMixin.class); + code.add("objectMapper.addMixIn($T.class, $T.class);\n", CustomPhaseConfig.class, + JacksonCustomPhaseConfigMixin.class); + for (Map.Entry solverConfigXmlEntry : solverXmlMap.entrySet()) { + code.add("solverConfigMap.put($S, objectMapper.readerFor($T.class).readValue($S));\n", + solverConfigXmlEntry.getKey(), + SolverConfig.class, solverConfigXmlEntry.getValue()); + } + code.add( + "$T.registerSolverConfigs(environment, beanFactory, $T.currentThread().getContextClassLoader(), solverConfigMap);\n", + TimefoldSolverAotContribution.class, Thread.class); + code.endControlFlow(); + code.beginControlFlow("catch ($T e)", JsonProcessingException.class); + code.add("throw new $T(e);\n", RuntimeException.class); + code.endControlFlow(); + builder.addCode(code.build()); }); - - // Make spring call our generated class when the native image starts - beanFactoryInitializationCode.addInitializer(new DefaultMethodReference( - MethodSpec.methodBuilder("registerSolverConfigs") - .addModifiers(Modifier.PUBLIC) - .addModifiers(Modifier.STATIC) - .addParameter(Environment.class, "environment") - .addParameter(ConfigurableListableBeanFactory.class, "beanFactory") - .build(), - generatedClass.getName())); + // Make spring call our generated method when the native image starts + beanFactoryInitializationCode.addInitializer(generatedMethod.toMethodReference()); } } diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java index 9681ada1bb..c942b38f09 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java @@ -33,9 +33,7 @@ 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.spring.boot.autoconfigure.config.SolverManagerProperties; 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; @@ -87,6 +85,7 @@ public class TimefoldSolverAutoConfiguration }; private ApplicationContext context; private ClassLoader beanClassLoader; + private Environment environment; private TimefoldProperties timefoldProperties; protected TimefoldSolverAutoConfiguration() { @@ -107,6 +106,7 @@ public void setEnvironment(Environment environment) { // postProcessBeanFactory runs before creating any bean, but we need TimefoldProperties. // Therefore, we use the Environment to load the properties BindResult result = Binder.get(environment).bind("timefold", TimefoldProperties.class); + this.environment = environment; this.timefoldProperties = result.orElseGet(TimefoldProperties::new); } @@ -153,26 +153,7 @@ public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableL @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { Map solverConfigMap = getSolverConfigMap(); - if (solverConfigMap.isEmpty()) { - beanFactory.registerSingleton(DEFAULT_SOLVER_CONFIG_NAME, new SolverConfig(beanClassLoader)); - return; - } - - if (timefoldProperties.getSolver() == null || timefoldProperties.getSolver().size() == 1) { - beanFactory.registerSingleton(DEFAULT_SOLVER_CONFIG_NAME, solverConfigMap.values().iterator().next()); - } 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)); - }); - } + TimefoldSolverAotContribution.registerSolverConfigs(environment, beanFactory, beanClassLoader, solverConfigMap); } private SolverConfig createSolverConfig(TimefoldProperties timefoldProperties, String solverName) { diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonCustomPhaseConfigMixin.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonCustomPhaseConfigMixin.java new file mode 100644 index 0000000000..e22d152e3f --- /dev/null +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonCustomPhaseConfigMixin.java @@ -0,0 +1,12 @@ +package ai.timefold.solver.spring.boot.autoconfigure.util; + +import java.util.List; + +import ai.timefold.solver.core.impl.phase.custom.CustomPhaseCommand; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public abstract class JacksonCustomPhaseConfigMixin { + @JsonIgnore + public abstract List getCustomPhaseCommandList(); +} diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonSolverConfigMixin.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonSolverConfigMixin.java new file mode 100644 index 0000000000..20a6854514 --- /dev/null +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonSolverConfigMixin.java @@ -0,0 +1,19 @@ +package ai.timefold.solver.spring.boot.autoconfigure.util; + +import java.util.Map; + +import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner; +import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public abstract class JacksonSolverConfigMixin { + @JsonIgnore + public abstract ClassLoader getClassLoader(); + + @JsonIgnore + public abstract Map getGizmoMemberAccessorMap(); + + @JsonIgnore + public abstract Map getGizmoSolutionClonerMap(); +} diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonTerminationConfigMixin.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonTerminationConfigMixin.java new file mode 100644 index 0000000000..797c8646e0 --- /dev/null +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonTerminationConfigMixin.java @@ -0,0 +1,8 @@ +package ai.timefold.solver.spring.boot.autoconfigure.util; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public abstract class JacksonTerminationConfigMixin { + @JsonIgnore + public abstract boolean isConfigured(); +} diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java deleted file mode 100644 index 27226cd66b..0000000000 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java +++ /dev/null @@ -1,419 +0,0 @@ -package ai.timefold.solver.spring.boot.autoconfigure.util; - -import java.lang.reflect.Array; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.RecordComponent; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; - -import org.apache.commons.text.StringEscapeUtils; -import org.springframework.javapoet.CodeBlock; -import org.springframework.javapoet.TypeSpec; - -public final class PojoInliner { - static final String COMPLEX_POJO_MAP_FIELD_NAME = "$pojoMap"; - private final Map complexPojoToIdentifier = new IdentityHashMap<>(); - private final Set possibleCircularRecordReferenceSet = Collections.newSetFromMap(new IdentityHashMap<>()); - private final CodeBlock.Builder initializerBuilder; - - // The code below uses CodeBlock.Builder to generate the Java file - // that stores the SolverConfig. - // CodeBlock.Builder.add supports different kinds of formatting args. - // The ones we use are: - // - $L: Format as is (i.e. literal replacement). - // - $S: Format as a Java String, doing the necessary escapes - // and surrounding it by double quotes. - // - $T: Format as a fully qualified type, which allows you to use - // classes without importing them. - PojoInliner() { - this.initializerBuilder = CodeBlock.builder(); - initializerBuilder.add("$T $L = new $T();", Map.class, COMPLEX_POJO_MAP_FIELD_NAME, HashMap.class); - } - - public record PojoField(Class type, String name, Object value) { - } - - public static PojoField field(Class type, String name, Object value) { - return new PojoField(type, name, value); - } - - public static void inlineFields(TypeSpec.Builder typeBuilder, PojoField... fields) { - PojoInliner inliner = new PojoInliner(); - for (PojoField field : fields) { - typeBuilder.addField(field.type(), field.name, - javax.lang.model.element.Modifier.PRIVATE, - javax.lang.model.element.Modifier.STATIC, - javax.lang.model.element.Modifier.FINAL); - } - for (PojoField field : fields) { - inliner.inlineField(field.name(), field.value()); - } - inliner.initializerBuilder.add("\n"); - typeBuilder.addStaticBlock(inliner.initializerBuilder.build()); - } - - void inlineField(String fieldName, Object fieldValue) { - initializerBuilder.add("\n$L = $L;", fieldName, getInlinedPojo(fieldValue)); - } - - /** - * Serializes a Pojo to code that uses its no-args constructor - * and setters to create the object. - * - * @param pojo The object to be serialized. - * @return A string that can be used in a {@link CodeBlock.Builder} to access the object - */ - String getInlinedPojo(Object pojo) { - // First, check for primitives - if (pojo == null) { - return "null"; - } - if (pojo instanceof Boolean value) { - return value.toString(); - } - if (pojo instanceof Byte value) { - // Cast to byte - return "((byte) " + value + ")"; - } - if (pojo instanceof Character value) { - return "'\\u" + Integer.toHexString(value | 0x10000).substring(1) + "'"; - } - if (pojo instanceof Short value) { - // Cast to short - return "((short) " + value + ")"; - } - if (pojo instanceof Integer value) { - return value.toString(); - } - if (pojo instanceof Long value) { - // Add long suffix to number string - return value + "L"; - } - if (pojo instanceof Float value) { - // Add float suffix to number string - return value + "f"; - } - if (pojo instanceof Double value) { - // Add double suffix to number string - return value + "d"; - } - - // Check for builtin classes - if (pojo instanceof String value) { - return "\"" + StringEscapeUtils.escapeJava(value) + "\""; - } - if (pojo instanceof Class value) { - if (!Modifier.isPublic(value.getModifiers())) { - throw new IllegalArgumentException("Cannot serialize (" + value + ") because it is not a public class."); - } - return value.getCanonicalName() + ".class"; - } - if (pojo instanceof ClassLoader) { - // We don't support serializing ClassLoaders, so replace it - // with the context class loader - return "Thread.currentThread().getContextClassLoader()"; - } - if (pojo instanceof Duration value) { - return Duration.class.getCanonicalName() + ".ofNanos(" + value.toNanos() + "L)"; - } - if (pojo.getClass().isEnum()) { - // Use field access to read the enum - Class enumClass = pojo.getClass(); - if (!Modifier.isPublic(enumClass.getModifiers())) { - throw new IllegalArgumentException( - "Cannot serialize (" + pojo + ") because its type (" + enumClass + ") is not a public class."); - } - Enum pojoEnum = (Enum) pojo; - return enumClass.getCanonicalName() + "." + pojoEnum.name(); - } - return getInlinedComplexPojo(pojo); - } - - public CodeBlock.Builder getInitializerBuilder() { - return initializerBuilder; - } - - /** - * Return a string that can be used in a {@link CodeBlock.Builder} to access a complex object - * - * @param pojo The object to be accessed - * @return A string that can be used in a {@link CodeBlock.Builder} to access the object. - */ - private String getPojoFromMap(Object pojo) { - return CodeBlock.builder().add("(($T) $L.get($S))", - pojo.getClass(), - COMPLEX_POJO_MAP_FIELD_NAME, - complexPojoToIdentifier.get(pojo)).build().toString(); - } - - /** - * Serializes collections and complex POJOs to code - */ - private String getInlinedComplexPojo(Object pojo) { - if (possibleCircularRecordReferenceSet.contains(pojo)) { - // Records do not have a no-args constructor, so we cannot safely serialize self-references in records - // as we cannot do a map lookup before the record is created. - throw new IllegalArgumentException( - "Cannot serialize record (" + pojo + ") because it is a record containing contains a circular reference."); - } - - // If we already serialized the object, we should just return - // the code string - if (complexPojoToIdentifier.containsKey(pojo)) { - return getPojoFromMap(pojo); - } - if (pojo instanceof Record value) { - // Records must set all fields at initialization time, - // so we delay the declaration of its variable - return getInlinedRecord(value); - } - // Object is not serialized yet - // Create a new variable to store its value when setting its fields - String newIdentifier = "$obj" + complexPojoToIdentifier.size(); - complexPojoToIdentifier.put(pojo, newIdentifier); - - // First, check if it is a collection type - if (pojo.getClass().isArray()) { - return getInlinedArray(newIdentifier, pojo); - } - if (pojo instanceof List value) { - return getInlinedList(newIdentifier, value); - } - if (pojo instanceof Set value) { - return getInlinedSet(newIdentifier, value); - } - if (pojo instanceof Map value) { - return getInlinedMap(newIdentifier, value); - } - - // Not a collection or record type, so serialize by creating a new instance and settings its fields - if (!Modifier.isPublic(pojo.getClass().getModifiers())) { - throw new IllegalArgumentException("Cannot serialize (" + pojo + ") because its type (" + pojo.getClass() - + ") is not public."); - } - initializerBuilder.add("\n$T $L;", pojo.getClass(), newIdentifier); - try { - Constructor constructor = pojo.getClass().getConstructor(); - if (!Modifier.isPublic(constructor.getModifiers())) { - throw new IllegalArgumentException("Cannot serialize (" + pojo + ") because its type's (" + pojo.getClass() - + ") no-args constructor is not public."); - } - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException("Cannot serialize (" + pojo + ") because its type (" + pojo.getClass() - + ") does not have a public no-args constructor."); - } - initializerBuilder.add("\n$L = new $T();", newIdentifier, pojo.getClass()); - initializerBuilder.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); - inlineFieldsOfPojo(pojo.getClass(), newIdentifier, pojo); - return getPojoFromMap(pojo); - } - - private String getInlinedArray(String newIdentifier, Object array) { - Class componentType = array.getClass().getComponentType(); - if (!Modifier.isPublic(componentType.getModifiers())) { - throw new IllegalArgumentException( - "Cannot serialize array of type (" + componentType + ") because (" + componentType + ") is not public."); - } - initializerBuilder.add("\n$T $L;", array.getClass(), newIdentifier); - - // Get the length of the array - int length = Array.getLength(array); - - // Create a new array from the component type with the given length - initializerBuilder.add("\n$L = new $T[$L];", newIdentifier, componentType, Integer.toString(length)); - initializerBuilder.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); - for (int i = 0; i < length; i++) { - // Set the elements of the array - initializerBuilder.add("\n$L[$L] = $L;", - newIdentifier, - Integer.toString(i), - getInlinedPojo(Array.get(array, i))); - } - return getPojoFromMap(array); - } - - private String getInlinedList(String newIdentifier, List list) { - initializerBuilder.add("\n$T $L;", List.class, newIdentifier); - - // Create an ArrayList - initializerBuilder.add("\n$L = new $T($L);", newIdentifier, ArrayList.class, Integer.toString(list.size())); - initializerBuilder.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); - for (Object item : list) { - // Add each item of the list to the ArrayList - initializerBuilder.add("\n$L.add($L);", - newIdentifier, - getInlinedPojo(item)); - } - return getPojoFromMap(list); - } - - private String getInlinedSet(String newIdentifier, Set set) { - initializerBuilder.add("\n$T $L;", Set.class, newIdentifier); - - // Create a new HashSet - initializerBuilder.add("\n$L = new $T($L);", newIdentifier, LinkedHashSet.class, Integer.toString(set.size())); - initializerBuilder.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); - for (Object item : set) { - // Add each item of the set to the HashSet - initializerBuilder.add("\n$L.add($L);", - newIdentifier, - getInlinedPojo(item)); - } - return getPojoFromMap(set); - } - - private String getInlinedMap(String newIdentifier, Map map) { - initializerBuilder.add("\n$T $L;", Map.class, newIdentifier); - - // Create a HashMap - initializerBuilder.add("\n$L = new $T($L);", newIdentifier, LinkedHashMap.class, Integer.toString(map.size())); - initializerBuilder.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); - for (Map.Entry entry : map.entrySet()) { - // Put each entry of the map into the HashMap - initializerBuilder.add("\n$L.put($L, $L);", - newIdentifier, - getInlinedPojo(entry.getKey()), - getInlinedPojo(entry.getValue())); - } - return getPojoFromMap(map); - } - - private String getInlinedRecord(Record record) { - possibleCircularRecordReferenceSet.add(record); - Class recordClass = record.getClass(); - if (!Modifier.isPublic(recordClass.getModifiers())) { - throw new IllegalArgumentException( - "Cannot serialize record (" + record + ") because its type (" + recordClass + ") is not public."); - } - - RecordComponent[] recordComponents = recordClass.getRecordComponents(); - String[] componentAccessors = new String[recordComponents.length]; - for (int i = 0; i < recordComponents.length; i++) { - Object value; - Class serializedType = getSerializedType(recordComponents[i].getType()); - if (!recordComponents[i].getType().equals(serializedType)) { - throw new IllegalArgumentException( - "Cannot serialize type (" + recordClass + ") as its component (" + recordComponents[i].getName() - + ") uses an implementation of a collection (" - + recordComponents[i].getType() + ") instead of the interface type (" + serializedType + ")."); - } - try { - value = recordComponents[i].getAccessor().invoke(record); - } catch (InvocationTargetException | IllegalAccessException e) { - throw new IllegalStateException(e); - } - try { - componentAccessors[i] = getInlinedPojo(value); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Cannot serialize record (" + record + ") because the value (" - + value + ") for its component (" + recordComponents[i].getName() + ") is not serializable.", e); - } - } - // All components serialized, so no circular references - possibleCircularRecordReferenceSet.remove(record); - StringBuilder constructorArgs = new StringBuilder(); - for (String componentAccessor : componentAccessors) { - constructorArgs.append(componentAccessor).append(", "); - } - if (componentAccessors.length != 0) { - constructorArgs.delete(constructorArgs.length() - 2, constructorArgs.length()); - } - String newIdentifier = "$obj" + complexPojoToIdentifier.size(); - complexPojoToIdentifier.put(record, newIdentifier); - initializerBuilder.add("\n$T $L = new $T($L);", recordClass, newIdentifier, recordClass, constructorArgs.toString()); - initializerBuilder.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier); - return getPojoFromMap(record); - } - - private Class getSerializedType(Class query) { - if (List.class.isAssignableFrom(query)) { - return List.class; - } - if (Set.class.isAssignableFrom(query)) { - return Set.class; - } - if (Map.class.isAssignableFrom(query)) { - return Map.class; - } - return query; - } - - /** - * Sets the fields of pojo declared in pojoClass and all its superclasses. - * - * @param pojoClass A class assignable to pojo containing some of its fields. - * @param identifier The name of the variable storing the serialized pojo. - * @param pojo The object being serialized. - */ - private void inlineFieldsOfPojo(Class pojoClass, String identifier, Object pojo) { - if (pojoClass == Object.class) { - // We are the top-level, no more fields to set - return; - } - Field[] fields = pojoClass.getDeclaredFields(); - // Sort by name to guarantee a consistent ordering - Arrays.sort(fields, Comparator.comparing(Field::getName)); - for (Field field : fields) { - if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) { - // We do not want to write static fields - continue; - } - // Set the field accessible so we can read its value - field.setAccessible(true); - Class serializedType = getSerializedType(field.getType()); - Method setterMethod = ReflectionHelper.getSetterMethod(pojoClass, serializedType, field.getName()); - // setterMethod guaranteed to be public - if (setterMethod == null) { - if (!field.getType().equals(serializedType)) { - throw new IllegalArgumentException( - "Cannot serialize type (" + pojoClass + ") as its field (" + field.getName() - + ") uses an implementation of a collection (" - + field.getType() + ") instead of the interface type (" + serializedType + ")."); - } - throw new IllegalArgumentException( - "Cannot serialize type (" + pojoClass + ") as it is missing a public setter method for field (" - + field.getName() + ") of type (" + field.getType() + ")."); - } - Object value; - try { - value = field.get(pojo); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - - try { - // Convert the field value to code, and call the setter - // corresponding to the field with the serialized field value. - initializerBuilder.add("\n$L.$L($L);", identifier, - setterMethod.getName(), - getInlinedPojo(value)); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Cannot serialize object (" + pojo + ") because the value (" + value - + ") for its field (" + field.getName() + ") is not serializable.", e); - } - } - try { - inlineFieldsOfPojo(pojoClass.getSuperclass(), identifier, pojo); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Cannot serialize type (" + pojoClass + ") because its superclass (" - + pojoClass.getSuperclass() + ") is not serializable.", e); - } - - } -} diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInlinerTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInlinerTest.java deleted file mode 100644 index b750a1b5fb..0000000000 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInlinerTest.java +++ /dev/null @@ -1,679 +0,0 @@ -package ai.timefold.solver.spring.boot.autoconfigure.util; - -import static ai.timefold.solver.spring.boot.autoconfigure.util.PojoInliner.COMPLEX_POJO_MAP_FIELD_NAME; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.lang.model.element.Modifier; - -import ai.timefold.solver.core.config.solver.EnvironmentMode; - -import org.junit.jupiter.api.Test; -import org.springframework.javapoet.CodeBlock; -import org.springframework.javapoet.FieldSpec; -import org.springframework.javapoet.TypeSpec; - -public class PojoInlinerTest { - private static class PrivatePojo { - @Override - public String toString() { - return "PrivatePojo()"; - } - } - - private record PrivateRecord() { - } - - private enum PrivateEnum { - VALUE - } - - public static class BasicPojo { - BasicPojo parentPojo; - int id; - String name; - - public BasicPojo() { - } - - public BasicPojo(BasicPojo parentPojo, int id, String name) { - this.parentPojo = parentPojo; - this.id = id; - this.name = name; - } - - public BasicPojo getParentPojo() { - return parentPojo; - } - - public void setParentPojo(BasicPojo parentPojo) { - this.parentPojo = parentPojo; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - public record RecordPojo(String operation, int recordId, BasicPojo pojo) { - } - - public static class LinkedRecordPojoReference { - LinkedRecordPojo reference; - - public LinkedRecordPojo getReference() { - return reference; - } - - public void setReference(LinkedRecordPojo reference) { - this.reference = reference; - } - - @Override - public String toString() { - return "LinkedRecordPojoReference()"; - } - } - - public record LinkedRecordPojo(LinkedRecordPojoReference next) { - public static LinkedRecordPojo circular() { - LinkedRecordPojoReference nextField = new LinkedRecordPojoReference(); - LinkedRecordPojo out = new LinkedRecordPojo(nextField); - nextField.setReference(out); - return out; - } - - public static LinkedRecordPojo nonCircular() { - LinkedRecordPojoReference nextField = new LinkedRecordPojoReference(); - nextField.setReference(new LinkedRecordPojo(null)); - return new LinkedRecordPojo(nextField); - } - } - - public record ArrayListRecord(ArrayList arrayList) { - } - - public static class ArrayListPojo { - ArrayList arrayList; - - public ArrayList getArrayList() { - return arrayList; - } - - public void setArrayList(ArrayList arrayList) { - this.arrayList = arrayList; - } - - @Override - public String toString() { - return "ArrayListPojo{" + - "arrayList=" + arrayList + - '}'; - } - } - - public static class NotPojo { - int aFieldWithSetter; - int bFieldWithoutSetter; - - public NotPojo() { - - } - - public NotPojo(int aFieldWithSetter, int bFieldWithoutSetter) { - this.aFieldWithSetter = aFieldWithSetter; - this.bFieldWithoutSetter = bFieldWithoutSetter; - } - - public int getAFieldWithSetter() { - return aFieldWithSetter; - } - - public void setAFieldWithSetter(int aFieldWithSetter) { - this.aFieldWithSetter = aFieldWithSetter; - } - - public int getBFieldWithoutSetter() { - return bFieldWithoutSetter; - } - } - - public static class PrivateSetterPojo { - int aFieldWithSetter; - int bFieldWithSetter; - - public PrivateSetterPojo() { - - } - - public PrivateSetterPojo(int aFieldWithSetter, int bFieldWithSetter) { - this.aFieldWithSetter = aFieldWithSetter; - this.bFieldWithSetter = bFieldWithSetter; - } - - public int getAFieldWithSetter() { - return aFieldWithSetter; - } - - public void setAFieldWithSetter(int aFieldWithSetter) { - this.aFieldWithSetter = aFieldWithSetter; - } - - public int getBFieldWithoutSetter() { - return bFieldWithSetter; - } - - private void setBFieldWithSetter(int bFieldWithSetter) { - this.bFieldWithSetter = bFieldWithSetter; - } - } - - public static class ExtendedPojo extends BasicPojo { - private String additionalField; - - public String getAdditionalField() { - return additionalField; - } - - public void setAdditionalField(String additionalField) { - this.additionalField = additionalField; - } - } - - public static class ExtendedNotPojo extends PrivateSetterPojo { - private String additionalField; - - public String getAdditionalField() { - return additionalField; - } - - public void setAdditionalField(String additionalField) { - this.additionalField = additionalField; - } - } - - void assertBuilder(CodeBlock.Builder builder, String expected) { - if (expected.isEmpty()) { - assertThat(builder.build().toString().trim()) - .isEqualTo("%s %s = new %s();".formatted(Map.class.getCanonicalName(), COMPLEX_POJO_MAP_FIELD_NAME, - HashMap.class.getCanonicalName())); - } else { - assertThat(builder.build().toString().trim()) - .isEqualTo("%s %s = new %s();\n".formatted(Map.class.getCanonicalName(), COMPLEX_POJO_MAP_FIELD_NAME, - HashMap.class.getCanonicalName()) + expected.trim()); - } - } - - private String getPojo(int id, Object value) { - return CodeBlock.builder().add("(($T) $L.get($S))", - value.getClass(), - COMPLEX_POJO_MAP_FIELD_NAME, - "$obj" + id).build().toString(); - } - - void assertAccessor(String accessor, int id, Object value) { - assertThat(accessor).isEqualTo(getPojo(id, value)); - } - - @Test - void inlinePrivateClasses() { - PojoInliner inliner = new PojoInliner(); - - assertThatCode(() -> inliner.getInlinedPojo(PrivatePojo.class)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize (" + PrivatePojo.class + ") because it is not a public class."); - - assertThatCode(() -> inliner.getInlinedPojo(new PrivatePojo())) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize (" + new PrivatePojo() + ") because its type (" + PrivatePojo.class - + ") is not public."); - - assertThatCode(() -> inliner.getInlinedPojo(new PrivateRecord())) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize record (" + new PrivateRecord() + ") because its type (" + PrivateRecord.class - + ") is not public."); - - assertThatCode(() -> inliner.getInlinedPojo(PrivateEnum.VALUE)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize (" + PrivateEnum.VALUE + ") because its type (" + PrivateEnum.class - + ") is not a public class."); - } - - @Test - void inlinePrivateSetter() { - PojoInliner inliner = new PojoInliner(); - - assertThatCode(() -> inliner.getInlinedPojo(new PrivateSetterPojo(1, 2))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize type (" + PrivateSetterPojo.class - + ") as it is missing a public setter method for field (bFieldWithSetter) of type (int)."); - } - - @Test - void inlineTypeUsingInterfaceImpl() { - PojoInliner inliner = new PojoInliner(); - - ArrayListPojo pojo = new ArrayListPojo(); - pojo.setArrayList(new ArrayList<>()); - assertThatCode(() -> inliner.getInlinedPojo(pojo)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize type (" + ArrayListPojo.class - + ") as its field (arrayList) uses an implementation of a collection (" + ArrayList.class - + ") instead of the interface type (" + List.class + ")."); - - ArrayListRecord record = new ArrayListRecord(new ArrayList<>()); - assertThatCode(() -> inliner.getInlinedPojo(record)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize type (" + ArrayListRecord.class - + ") as its component (arrayList) uses an implementation of a collection (" + ArrayList.class - + ") instead of the interface type (" + List.class + ")."); - } - - @Test - void inlineNotPojo() { - PojoInliner inliner = new PojoInliner(); - - NotPojo pojo = new NotPojo(1, 2); - assertThatCode(() -> inliner.getInlinedPojo(pojo)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize type (" + NotPojo.class - + ") as it is missing a public setter method for field (bFieldWithoutSetter) of type (int)."); - } - - @Test - void inlineExtendedNotPojo() { - PojoInliner inliner = new PojoInliner(); - - assertThatCode(() -> inliner.getInlinedPojo(new ExtendedNotPojo())) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize type (" + ExtendedNotPojo.class + ") because its superclass (" - + PrivateSetterPojo.class + ") is not serializable.") - .cause() - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize type (" + PrivateSetterPojo.class - + ") as it is missing a public setter method for field (bFieldWithSetter) of type (int)."); - } - - @Test - void inlineCircularRecord() { - PojoInliner inliner = new PojoInliner(); - - LinkedRecordPojo pojo = LinkedRecordPojo.circular(); - - assertThatCode(() -> inliner.getInlinedPojo(pojo)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize record (" + pojo + ") because the value (" + pojo.next - + ") for its component (next) is not serializable.") - .cause() - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize object (" + pojo.next + ") because the value (" + pojo - + ") for its field (reference) is not serializable.") - .cause() - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot serialize record (" + pojo - + ") because it is a record containing contains a circular reference."); - } - - @Test - void inlinePrimitives() { - PojoInliner inliner = new PojoInliner(); - - // null - assertThat(inliner.getInlinedPojo(null)).isEqualTo("null"); - assertBuilder(inliner.getInitializerBuilder(), ""); - - // numbers - assertThat(inliner.getInlinedPojo(true)).isEqualTo("true"); - assertBuilder(inliner.getInitializerBuilder(), ""); - assertThat(inliner.getInlinedPojo(false)).isEqualTo("false"); - assertBuilder(inliner.getInitializerBuilder(), ""); - assertThat(inliner.getInlinedPojo((byte) 1)).isEqualTo("((byte) 1)"); - assertBuilder(inliner.getInitializerBuilder(), ""); - assertThat(inliner.getInlinedPojo((short) 1)).isEqualTo("((short) 1)"); - assertBuilder(inliner.getInitializerBuilder(), ""); - assertThat(inliner.getInlinedPojo(1)).isEqualTo("1"); - assertBuilder(inliner.getInitializerBuilder(), ""); - assertThat(inliner.getInlinedPojo(1L)).isEqualTo("1L"); - assertBuilder(inliner.getInitializerBuilder(), ""); - assertThat(inliner.getInlinedPojo(1f)).isEqualTo("1.0f"); - assertBuilder(inliner.getInitializerBuilder(), ""); - assertThat(inliner.getInlinedPojo(1d)).isEqualTo("1.0d"); - assertBuilder(inliner.getInitializerBuilder(), ""); - - // Strings and chars - assertThat(inliner.getInlinedPojo('a')).isEqualTo("'\\u0061'"); - assertBuilder(inliner.getInitializerBuilder(), ""); - assertThat(inliner.getInlinedPojo("my\nmultiline\nstring")).isEqualTo("\"my\\nmultiline\\nstring\""); - assertBuilder(inliner.getInitializerBuilder(), ""); - } - - @Test - void inlineObjectPrimitives() { - PojoInliner inliner = new PojoInliner(); - - // Classes - assertThat(inliner.getInlinedPojo(PojoInliner.class)) - .isEqualTo(PojoInliner.class.getCanonicalName() + ".class"); - assertBuilder(inliner.getInitializerBuilder(), ""); - - // Enums - assertThat(inliner.getInlinedPojo(EnvironmentMode.REPRODUCIBLE)) - .isEqualTo(EnvironmentMode.class.getCanonicalName() + "." + EnvironmentMode.REPRODUCIBLE.name()); - assertBuilder(inliner.getInitializerBuilder(), ""); - - // ClassLoader - assertThat(inliner.getInlinedPojo(Thread.currentThread().getContextClassLoader())) - .isEqualTo("Thread.currentThread().getContextClassLoader()"); - assertBuilder(inliner.getInitializerBuilder(), ""); - } - - @Test - void inlinePrimitiveArray() { - PojoInliner inliner = new PojoInliner(); - - int[] pojo = new int[] { 1, 2, 3, 4, 5 }; - String accessor = inliner.getInlinedPojo(pojo); - assertBuilder(inliner.getInitializerBuilder(), - """ - int[] $obj0; - $obj0 = new int[5]; - $pojoMap.put("$obj0", $obj0); - $obj0[0] = 1; - $obj0[1] = 2; - $obj0[2] = 3; - $obj0[3] = 4; - $obj0[4] = 5; - """); - assertAccessor(accessor, 0, pojo); - } - - @Test - void inlineObjectArray() { - PojoInliner inliner = new PojoInliner(); - - Object[] pojo = new Object[3]; - pojo[0] = null; - pojo[1] = pojo; - pojo[2] = "Item"; - String accessor = inliner.getInlinedPojo(pojo); - assertBuilder(inliner.getInitializerBuilder(), - """ - java.lang.Object[] $obj0; - $obj0 = new java.lang.Object[3]; - $pojoMap.put("$obj0", $obj0); - $obj0[0] = null; - $obj0[1] = %s; - $obj0[2] = \"Item\"; - """.formatted(getPojo(0, pojo))); - assertAccessor(accessor, 0, pojo); - } - - @Test - void inlineIntList() { - PojoInliner inliner = new PojoInliner(); - - List pojo = List.of(1, 2, 3); - String accessor = inliner.getInlinedPojo(pojo); - assertBuilder(inliner.getInitializerBuilder(), - """ - java.util.List $obj0; - $obj0 = new java.util.ArrayList(3); - $pojoMap.put("$obj0", $obj0); - $obj0.add(1); - $obj0.add(2); - $obj0.add(3); - """); - assertAccessor(accessor, 0, pojo); - } - - @Test - void inlineIntSet() { - PojoInliner inliner = new PojoInliner(); - - Set pojo = new LinkedHashSet<>(3); - pojo.add(1); - pojo.add(2); - pojo.add(3); - String accessor = inliner.getInlinedPojo(pojo); - assertBuilder(inliner.getInitializerBuilder(), - """ - java.util.Set $obj0; - $obj0 = new java.util.LinkedHashSet(3); - $pojoMap.put("$obj0", $obj0); - $obj0.add(1); - $obj0.add(2); - $obj0.add(3); - """); - assertAccessor(accessor, 0, pojo); - } - - @Test - void inlineIntStringMap() { - PojoInliner inliner = new PojoInliner(); - - Map pojo = new LinkedHashMap<>(3); - pojo.put(1, "a"); - pojo.put(2, "b"); - pojo.put(3, "c"); - String accessor = inliner.getInlinedPojo(pojo); - assertBuilder(inliner.getInitializerBuilder(), - """ - java.util.Map $obj0; - $obj0 = new java.util.LinkedHashMap(3); - $pojoMap.put("$obj0", $obj0); - $obj0.put(1, \"a\"); - $obj0.put(2, \"b\"); - $obj0.put(3, \"c\"); - """); - assertAccessor(accessor, 0, pojo); - } - - @Test - void inlinePojo() { - PojoInliner inliner = new PojoInliner(); - - BasicPojo pojo = new BasicPojo(new BasicPojo(null, 0, "parent"), 1, "child"); - String accessor = inliner.getInlinedPojo(pojo); - String expected = """ - %s $obj0; - $obj0 = new %s(); - $pojoMap.put("$obj0", $obj0); - $obj0.setId(1); - $obj0.setName("child"); - %s $obj1; - $obj1 = new %s(); - $pojoMap.put("$obj1", $obj1); - $obj1.setId(0); - $obj1.setName("parent"); - $obj1.setParentPojo(null); - $obj0.setParentPojo(%s); - """.formatted(BasicPojo.class.getCanonicalName(), - BasicPojo.class.getCanonicalName(), - BasicPojo.class.getCanonicalName(), - BasicPojo.class.getCanonicalName(), - getPojo(1, pojo.getParentPojo())); - assertBuilder(inliner.getInitializerBuilder(), expected); - assertAccessor(accessor, 0, pojo); - String parentAccessor = inliner.getInlinedPojo(pojo.getParentPojo()); - assertBuilder(inliner.getInitializerBuilder(), expected); - assertAccessor(parentAccessor, 1, pojo.getParentPojo()); - } - - @Test - void inlineExtendedPojo() { - PojoInliner inliner = new PojoInliner(); - - ExtendedPojo pojo = new ExtendedPojo(); - pojo.setAdditionalField("newField"); - pojo.setId(1); - pojo.setName("child"); - pojo.setParentPojo(new BasicPojo(null, 0, "parent")); - String accessor = inliner.getInlinedPojo(pojo); - String expected = """ - %s $obj0; - $obj0 = new %s(); - $pojoMap.put("$obj0", $obj0); - $obj0.setAdditionalField("newField"); - $obj0.setId(1); - $obj0.setName("child"); - %s $obj1; - $obj1 = new %s(); - $pojoMap.put("$obj1", $obj1); - $obj1.setId(0); - $obj1.setName("parent"); - $obj1.setParentPojo(null); - $obj0.setParentPojo(%s); - """.formatted(ExtendedPojo.class.getCanonicalName(), - ExtendedPojo.class.getCanonicalName(), - BasicPojo.class.getCanonicalName(), - BasicPojo.class.getCanonicalName(), - getPojo(1, pojo.getParentPojo())); - assertBuilder(inliner.getInitializerBuilder(), expected); - assertAccessor(accessor, 0, pojo); - String parentAccessor = inliner.getInlinedPojo(pojo.getParentPojo()); - assertBuilder(inliner.getInitializerBuilder(), expected); - assertAccessor(parentAccessor, 1, pojo.getParentPojo()); - } - - @Test - void inlineRecord() { - PojoInliner inliner = new PojoInliner(); - - BasicPojo pojo = new BasicPojo(null, 0, "name"); - RecordPojo recordPojo = new RecordPojo("INSERT", 0, pojo); - String accessor = inliner.getInlinedPojo(recordPojo); - String expected = """ - %s $obj0; - $obj0 = new %s(); - $pojoMap.put("$obj0", $obj0); - $obj0.setId(0); - $obj0.setName("name"); - $obj0.setParentPojo(null); - %s $obj1 = new %s("INSERT", 0, %s); - $pojoMap.put("$obj1", $obj1); - """.formatted(BasicPojo.class.getCanonicalName(), - BasicPojo.class.getCanonicalName(), - RecordPojo.class.getCanonicalName(), - RecordPojo.class.getCanonicalName(), - getPojo(0, recordPojo.pojo())); - assertBuilder(inliner.getInitializerBuilder(), expected); - assertAccessor(accessor, 1, recordPojo); - String partAccessor = inliner.getInlinedPojo(recordPojo.pojo()); - assertBuilder(inliner.getInitializerBuilder(), expected); - assertAccessor(partAccessor, 0, recordPojo.pojo()); - } - - @Test - void inlineNonCircularRecord() { - PojoInliner inliner = new PojoInliner(); - - LinkedRecordPojo pojo = LinkedRecordPojo.nonCircular(); - String expected = """ - %s $obj0; - $obj0 = new %s(); - $pojoMap.put("$obj0", $obj0); - %s $obj1 = new %s(null); - $pojoMap.put("$obj1", $obj1); - $obj0.setReference(%s); - %s $obj2 = new %s(%s); - $pojoMap.put("$obj2", $obj2); - """.formatted(LinkedRecordPojoReference.class.getCanonicalName(), - LinkedRecordPojoReference.class.getCanonicalName(), - LinkedRecordPojo.class.getCanonicalName(), - LinkedRecordPojo.class.getCanonicalName(), - getPojo(1, pojo.next().getReference()), - LinkedRecordPojo.class.getCanonicalName(), - LinkedRecordPojo.class.getCanonicalName(), - getPojo(0, pojo.next())); - String accessor = inliner.getInlinedPojo(pojo); - assertBuilder(inliner.getInitializerBuilder(), expected); - assertAccessor(accessor, 2, pojo); - String nextAccessor = inliner.getInlinedPojo(pojo.next()); - assertBuilder(inliner.getInitializerBuilder(), expected); - assertAccessor(nextAccessor, 0, pojo.next()); - String referenceAccessor = inliner.getInlinedPojo(pojo.next().getReference()); - assertBuilder(inliner.getInitializerBuilder(), expected); - assertAccessor(referenceAccessor, 1, pojo.next().getReference()); - } - - @Test - void inlineFieldToStaticBlock() { - PojoInliner inliner = new PojoInliner(); - inliner.inlineField("myField", new BasicPojo(null, 0, "name")); - String expected = """ - %s $obj0; - $obj0 = new %s(); - $pojoMap.put("$obj0", $obj0); - $obj0.setId(0); - $obj0.setName("name"); - $obj0.setParentPojo(null); - myField = %s; - """.formatted(BasicPojo.class.getCanonicalName(), - BasicPojo.class.getCanonicalName(), - getPojo(0, new BasicPojo(null, 0, "name"))); - assertBuilder(inliner.getInitializerBuilder(), expected); - } - - @Test - void inlineMultipleFieldsToStaticBlock() { - var typeBuilder = TypeSpec.classBuilder("TestClass"); - PojoInliner.inlineFields(typeBuilder, - PojoInliner.field(int.class, "a", 1), - PojoInliner.field(BasicPojo.class, "b", new BasicPojo(null, 0, "name")), - PojoInliner.field(Object.class, "c", "text")); - TypeSpec typeSpec = typeBuilder.build(); - assertThat(typeSpec.fieldSpecs).containsExactly( - FieldSpec.builder(int.class, "a", Modifier.PRIVATE, - Modifier.STATIC, - Modifier.FINAL) - .build(), - FieldSpec.builder(BasicPojo.class, "b", Modifier.PRIVATE, - Modifier.STATIC, - Modifier.FINAL) - .build(), - FieldSpec.builder(Object.class, "c", Modifier.PRIVATE, - Modifier.STATIC, - Modifier.FINAL) - .build()); - assertThat(typeSpec.staticBlock.toString()).isEqualTo( - """ - static { - %s %s = new %s(); - a = 1; - %s $obj0; - $obj0 = new %s(); - $pojoMap.put("$obj0", $obj0); - $obj0.setId(0); - $obj0.setName("name"); - $obj0.setParentPojo(null); - b = %s; - c = "text"; - } - """.formatted(Map.class.getCanonicalName(), - COMPLEX_POJO_MAP_FIELD_NAME, - HashMap.class.getCanonicalName(), - BasicPojo.class.getCanonicalName(), - BasicPojo.class.getCanonicalName(), - getPojo(0, new BasicPojo(null, 0, "name")))); - } -} From 696803324a034888fb95cc26e94bab9e6292b8c9 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 13 Feb 2024 16:17:07 -0500 Subject: [PATCH 09/12] chore: Use Spring idioms, add XML reflection config files --- .../spring-boot-autoconfigure/pom.xml | 9 +- .../TimefoldSolverAotContribution.java | 143 +- .../TimefoldSolverAotFactory.java | 42 + .../TimefoldSolverAutoConfiguration.java | 52 +- .../util/JacksonCustomPhaseConfigMixin.java | 12 - .../util/JacksonSolverConfigMixin.java | 19 - .../util/JacksonTerminationConfigMixin.java | 8 - .../proxy-config.json | 8 + .../reflect-config.json | 2989 +++++++++++++++++ 9 files changed, 3092 insertions(+), 190 deletions(-) create mode 100644 spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotFactory.java delete mode 100644 spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonCustomPhaseConfigMixin.java delete mode 100644 spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonSolverConfigMixin.java delete mode 100644 spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonTerminationConfigMixin.java create mode 100644 spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/proxy-config.json create mode 100644 spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/reflect-config.json diff --git a/spring-integration/spring-boot-autoconfigure/pom.xml b/spring-integration/spring-boot-autoconfigure/pom.xml index 71b31dad27..0de50c0bb1 100644 --- a/spring-integration/spring-boot-autoconfigure/pom.xml +++ b/spring-integration/spring-boot-autoconfigure/pom.xml @@ -64,17 +64,10 @@ - - com.fasterxml.jackson.core - jackson-annotations - - - com.fasterxml.jackson.core - jackson-core - com.fasterxml.jackson.core jackson-databind + true diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java index 2903037df0..47a3e74f8e 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java @@ -1,48 +1,17 @@ package ai.timefold.solver.spring.boot.autoconfigure; -import java.lang.reflect.Field; -import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.Map; -import java.util.Set; -import ai.timefold.solver.core.api.solver.SolverFactory; -import ai.timefold.solver.core.api.solver.SolverManager; -import ai.timefold.solver.core.config.phase.custom.CustomPhaseConfig; 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.spring.boot.autoconfigure.config.SolverManagerProperties; -import ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties; -import ai.timefold.solver.spring.boot.autoconfigure.util.JacksonCustomPhaseConfigMixin; -import ai.timefold.solver.spring.boot.autoconfigure.util.JacksonSolverConfigMixin; -import ai.timefold.solver.spring.boot.autoconfigure.util.JacksonTerminationConfigMixin; -import org.springframework.aot.generate.GeneratedMethod; 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; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.context.properties.bind.BindResult; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.core.env.Environment; -import org.springframework.javapoet.CodeBlock; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; public class TimefoldSolverAotContribution implements BeanFactoryInitializationAotContribution { - private static final String DEFAULT_SOLVER_CONFIG_NAME = "getSolverConfig"; - - /** - * Map of SolverConfigs that were recorded during the build. - */ private final Map solverConfigMap; - private final static Set> BANNED_CLASSES = Set.of( - Class.class, - ClassLoader.class); public TimefoldSolverAotContribution(Map solverConfigMap) { this.solverConfigMap = solverConfigMap; @@ -66,117 +35,15 @@ private void registerType(ReflectionHints reflectionHints, Class type) { MemberCategory.INVOKE_PUBLIC_METHODS); } - private void registerTypeRecursively(ReflectionHints reflectionHints, Class type, Set> visited) { - if (type == null || BANNED_CLASSES.contains(type) || visited.contains(type)) { - return; - } - visited.add(type); - registerType(reflectionHints, type); - for (Field field : type.getDeclaredFields()) { - registerTypeRecursively(reflectionHints, field.getType(), visited); - } - registerTypeRecursively(reflectionHints, type.getSuperclass(), visited); - } - - public static void registerSolverConfigs(Environment environment, - ConfigurableListableBeanFactory beanFactory, - ClassLoader classLoader, - Map solverConfigMap) { - BindResult result = Binder.get(environment).bind("timefold", TimefoldProperties.class); - TimefoldProperties timefoldProperties = result.orElseGet(TimefoldProperties::new); - if (solverConfigMap.isEmpty()) { - beanFactory.registerSingleton(DEFAULT_SOLVER_CONFIG_NAME, new SolverConfig(classLoader)); - return; - } - - if (timefoldProperties.getSolver() == null || timefoldProperties.getSolver().size() == 1) { - beanFactory.registerSingleton(DEFAULT_SOLVER_CONFIG_NAME, solverConfigMap.values().iterator().next()); - } 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)); - }); - } - } - - // The code below uses CodeBlock.Builder to generate the Java file - // that stores the SolverConfig. - // CodeBlock.Builder.add supports different kinds of formatting args. - // The ones we use are: - // - $L: Format as is (i.e. literal replacement). - // - $S: Format as a Java String, doing the necessary escapes - // and surrounding it by double quotes. - // - $T: Format as a fully qualified type, which allows you to use - // classes without importing them. @Override public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { - var reflectionHints = generationContext.getRuntimeHints().reflection(); - - // Register all classes reachable from the SolverConfig for reflection - // (so we can read their metadata) - Set> classSet = new HashSet<>(); - Map solverXmlMap = new LinkedHashMap<>(); - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.addMixIn(SolverConfig.class, JacksonSolverConfigMixin.class); - objectMapper.addMixIn(TerminationConfig.class, JacksonTerminationConfigMixin.class); - objectMapper.addMixIn(CustomPhaseConfig.class, JacksonCustomPhaseConfigMixin.class); - for (Map.Entry solverConfigEntry : solverConfigMap.entrySet()) { - solverConfigEntry.getValue().visitReferencedClasses(clazz -> { - if (clazz != null) { - classSet.add(clazz); + ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection(); + for (SolverConfig solverConfig : solverConfigMap.values()) { + solverConfig.visitReferencedClasses(type -> { + if (type != null) { + registerType(reflectionHints, type); } }); - try { - solverXmlMap.put(solverConfigEntry.getKey(), objectMapper.writeValueAsString(solverConfigEntry.getValue())); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } } - registerType(reflectionHints, SolverConfig.class); - registerTypeRecursively(reflectionHints, SolverConfig.class, new HashSet<>()); - - for (Class clazz : classSet) { - registerType(reflectionHints, clazz); - } - - // Create a generated class to hold all the solver configs - GeneratedMethod generatedMethod = beanFactoryInitializationCode.getMethods().add("getSolverConfigs", - builder -> { - builder.addParameter(Environment.class, "environment"); - builder.addParameter(ConfigurableListableBeanFactory.class, "beanFactory"); - var code = CodeBlock.builder(); - code.beginControlFlow("try"); - code.add("$T<$T, $T> solverConfigMap = new $T<>();\n", Map.class, String.class, SolverConfig.class, - LinkedHashMap.class); - code.add("$T objectMapper = new $T();\n", ObjectMapper.class, ObjectMapper.class); - code.add("objectMapper.addMixIn($T.class, $T.class);\n", SolverConfig.class, - JacksonSolverConfigMixin.class); - code.add("objectMapper.addMixIn($T.class, $T.class);\n", TerminationConfig.class, - JacksonTerminationConfigMixin.class); - code.add("objectMapper.addMixIn($T.class, $T.class);\n", CustomPhaseConfig.class, - JacksonCustomPhaseConfigMixin.class); - for (Map.Entry solverConfigXmlEntry : solverXmlMap.entrySet()) { - code.add("solverConfigMap.put($S, objectMapper.readerFor($T.class).readValue($S));\n", - solverConfigXmlEntry.getKey(), - SolverConfig.class, solverConfigXmlEntry.getValue()); - } - code.add( - "$T.registerSolverConfigs(environment, beanFactory, $T.currentThread().getContextClassLoader(), solverConfigMap);\n", - TimefoldSolverAotContribution.class, Thread.class); - code.endControlFlow(); - code.beginControlFlow("catch ($T e)", JsonProcessingException.class); - code.add("throw new $T(e);\n", RuntimeException.class); - code.endControlFlow(); - builder.addCode(code.build()); - }); - // Make spring call our generated method when the native image starts - beanFactoryInitializationCode.addInitializer(generatedMethod.toMethodReference()); } } 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/TimefoldSolverAutoConfiguration.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java index c942b38f09..c87b48b92a 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java @@ -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; @@ -34,6 +35,7 @@ import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; +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; @@ -44,8 +46,10 @@ import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; -import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +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; @@ -59,14 +63,14 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; -@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 TimefoldSolverAutoConfiguration implements BeanClassLoaderAware, ApplicationContextAware, EnvironmentAware, BeanFactoryInitializationAotProcessor, - BeanFactoryPostProcessor { + BeanDefinitionRegistryPostProcessor { private static final Log LOG = LogFactory.getLog(TimefoldSolverAutoConfiguration.class); private static final String DEFAULT_SOLVER_CONFIG_NAME = "getSolverConfig"; @@ -151,9 +155,47 @@ public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableL } @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { Map solverConfigMap = getSolverConfigMap(); - TimefoldSolverAotContribution.registerSolverConfigs(environment, beanFactory, beanClassLoader, solverConfigMap); + BindResult result = Binder.get(environment).bind("timefold", TimefoldProperties.class); + TimefoldProperties timefoldProperties = result.orElseGet(TimefoldProperties::new); + 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) { + 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) -> { + 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); + }); + } } private SolverConfig createSolverConfig(TimefoldProperties timefoldProperties, String solverName) { diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonCustomPhaseConfigMixin.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonCustomPhaseConfigMixin.java deleted file mode 100644 index e22d152e3f..0000000000 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonCustomPhaseConfigMixin.java +++ /dev/null @@ -1,12 +0,0 @@ -package ai.timefold.solver.spring.boot.autoconfigure.util; - -import java.util.List; - -import ai.timefold.solver.core.impl.phase.custom.CustomPhaseCommand; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -public abstract class JacksonCustomPhaseConfigMixin { - @JsonIgnore - public abstract List getCustomPhaseCommandList(); -} diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonSolverConfigMixin.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonSolverConfigMixin.java deleted file mode 100644 index 20a6854514..0000000000 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonSolverConfigMixin.java +++ /dev/null @@ -1,19 +0,0 @@ -package ai.timefold.solver.spring.boot.autoconfigure.util; - -import java.util.Map; - -import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner; -import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -public abstract class JacksonSolverConfigMixin { - @JsonIgnore - public abstract ClassLoader getClassLoader(); - - @JsonIgnore - public abstract Map getGizmoMemberAccessorMap(); - - @JsonIgnore - public abstract Map getGizmoSolutionClonerMap(); -} diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonTerminationConfigMixin.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonTerminationConfigMixin.java deleted file mode 100644 index 797c8646e0..0000000000 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/JacksonTerminationConfigMixin.java +++ /dev/null @@ -1,8 +0,0 @@ -package ai.timefold.solver.spring.boot.autoconfigure.util; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -public abstract class JacksonTerminationConfigMixin { - @JsonIgnore - public abstract boolean isConfigured(); -} 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..cd57aeeefa --- /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,2989 @@ +[ + { + "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.benchmark.api.PlannerBenchmarkFactory" + }, + { + "name": "ai.timefold.solver.core.api.domain.common.DomainAccessType", + "allDeclaredFields": true, + "fields": [ + { + "name": "GIZMO" + }, + { + "name": "REFLECTION" + } + ] + }, + { + "name": "ai.timefold.solver.core.api.domain.entity.PlanningEntity", + "queryAllDeclaredMethods": true + }, + { + "name": "ai.timefold.solver.core.api.domain.solution.PlanningSolution", + "queryAllDeclaredMethods": true + }, + { + "name": "ai.timefold.solver.core.api.score.Score", + "queryAllDeclaredMethods": true + }, + { + "name": "ai.timefold.solver.core.api.score.ScoreManager", + "queryAllDeclaredMethods": true + }, + { + "name": "ai.timefold.solver.core.api.score.buildin.simple.SimpleScore", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType", + "allDeclaredFields": true, + "fields": [ + { + "name": "BAVET" + }, + { + "name": "DROOLS" + } + ] + }, + { + "name": "ai.timefold.solver.core.api.solver.SolutionManager", + "queryAllDeclaredMethods": true + }, + { + "name": "ai.timefold.solver.core.api.solver.SolverFactory", + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "methods": [ + { + "name": "buildSolver", + "parameterTypes": [] + } + ] + }, + { + "name": "ai.timefold.solver.core.api.solver.SolverManager", + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "close", + "parameterTypes": [] + } + ] + }, + { + "name": "ai.timefold.solver.core.config.AbstractConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getAcceptedCountLimit", + "parameterTypes": [] + }, + { + "name": "getAcceptorConfig", + "parameterTypes": [] + }, + { + "name": "getAcceptorTypeList", + "parameterTypes": [] + }, + { + "name": "getAssertionScoreDirectorFactory", + "parameterTypes": [] + }, + { + "name": "getBestScoreFeasible", + "parameterTypes": [] + }, + { + "name": "getBestScoreLimit", + "parameterTypes": [] + }, + { + "name": "getBetaDistributionAlpha", + "parameterTypes": [] + }, + { + "name": "getBetaDistributionBeta", + "parameterTypes": [] + }, + { + "name": "getBlockDistributionSizeMaximum", + "parameterTypes": [] + }, + { + "name": "getBlockDistributionSizeMinimum", + "parameterTypes": [] + }, + { + "name": "getBlockDistributionSizeRatio", + "parameterTypes": [] + }, + { + "name": "getBlockDistributionUniformDistributionProbability", + "parameterTypes": [] + }, + { + "name": "getBreakTieRandomly", + "parameterTypes": [] + }, + { + "name": "getCacheType", + "parameterTypes": [] + }, + { + "name": "getClassLoader", + "parameterTypes": [] + }, + { + "name": "getConstraintProviderClass", + "parameterTypes": [] + }, + { + "name": "getConstraintProviderCustomProperties", + "parameterTypes": [] + }, + { + "name": "getConstraintStreamImplType", + "parameterTypes": [] + }, + { + "name": "getConstraintStreamShareLambdas", + "parameterTypes": [] + }, + { + "name": "getConstructionHeuristicType", + "parameterTypes": [] + }, + { + "name": "getCustomPhaseCommandClassList", + "parameterTypes": [] + }, + { + "name": "getCustomPhaseCommandList", + "parameterTypes": [] + }, + { + "name": "getCustomProperties", + "parameterTypes": [] + }, + { + "name": "getDaemon", + "parameterTypes": [] + }, + { + "name": "getDaysSpentLimit", + "parameterTypes": [] + }, + { + "name": "getDestinationSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getDomainAccessType", + "parameterTypes": [] + }, + { + "name": "getDowncastEntityClass", + "parameterTypes": [] + }, + { + "name": "getEasyScoreCalculatorClass", + "parameterTypes": [] + }, + { + "name": "getEasyScoreCalculatorCustomProperties", + "parameterTypes": [] + }, + { + "name": "getEntityClass", + "parameterTypes": [] + }, + { + "name": "getEntityClassList", + "parameterTypes": [] + }, + { + "name": "getEntityPlacerConfig", + "parameterTypes": [] + }, + { + "name": "getEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getEntitySorterManner", + "parameterTypes": [] + }, + { + "name": "getEntityTabuRatio", + "parameterTypes": [] + }, + { + "name": "getEntityTabuSize", + "parameterTypes": [] + }, + { + "name": "getEnvironmentMode", + "parameterTypes": [] + }, + { + "name": "getExhaustiveSearchType", + "parameterTypes": [] + }, + { + "name": "getFadingEntityTabuRatio", + "parameterTypes": [] + }, + { + "name": "getFadingEntityTabuSize", + "parameterTypes": [] + }, + { + "name": "getFadingMoveTabuSize", + "parameterTypes": [] + }, + { + "name": "getFadingUndoMoveTabuSize", + "parameterTypes": [] + }, + { + "name": "getFadingValueTabuRatio", + "parameterTypes": [] + }, + { + "name": "getFadingValueTabuSize", + "parameterTypes": [] + }, + { + "name": "getFilterClass", + "parameterTypes": [] + }, + { + "name": "getFinalistPodiumType", + "parameterTypes": [] + }, + { + "name": "getFixedProbabilityWeight", + "parameterTypes": [] + }, + { + "name": "getForagerConfig", + "parameterTypes": [] + }, + { + "name": "getGizmoMemberAccessorMap", + "parameterTypes": [] + }, + { + "name": "getGizmoSolutionClonerMap", + "parameterTypes": [] + }, + { + "name": "getGreatDelugeWaterLevelIncrementRatio", + "parameterTypes": [] + }, + { + "name": "getGreatDelugeWaterLevelIncrementScore", + "parameterTypes": [] + }, + { + "name": "getHoursSpentLimit", + "parameterTypes": [] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "getIgnoreEmptyChildIterators", + "parameterTypes": [] + }, + { + "name": "getIncrementalScoreCalculatorClass", + "parameterTypes": [] + }, + { + "name": "getIncrementalScoreCalculatorCustomProperties", + "parameterTypes": [] + }, + { + "name": "getInitializingScoreTrend", + "parameterTypes": [] + }, + { + "name": "getLateAcceptanceSize", + "parameterTypes": [] + }, + { + "name": "getLinearDistributionSizeMaximum", + "parameterTypes": [] + }, + { + "name": "getLocalSearchType", + "parameterTypes": [] + }, + { + "name": "getMaximumK", + "parameterTypes": [] + }, + { + "name": "getMaximumSubChainSize", + "parameterTypes": [] + }, + { + "name": "getMaximumSubListSize", + "parameterTypes": [] + }, + { + "name": "getMaximumSubPillarSize", + "parameterTypes": [] + }, + { + "name": "getMillisecondsSpentLimit", + "parameterTypes": [] + }, + { + "name": "getMimicSelectorRef", + "parameterTypes": [] + }, + { + "name": "getMinimumK", + "parameterTypes": [] + }, + { + "name": "getMinimumSubChainSize", + "parameterTypes": [] + }, + { + "name": "getMinimumSubListSize", + "parameterTypes": [] + }, + { + "name": "getMinimumSubPillarSize", + "parameterTypes": [] + }, + { + "name": "getMinutesSpentLimit", + "parameterTypes": [] + }, + { + "name": "getMonitoringConfig", + "parameterTypes": [] + }, + { + "name": "getMoveIteratorFactoryClass", + "parameterTypes": [] + }, + { + "name": "getMoveIteratorFactoryCustomProperties", + "parameterTypes": [] + }, + { + "name": "getMoveListFactoryClass", + "parameterTypes": [] + }, + { + "name": "getMoveListFactoryCustomProperties", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorConfigList", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorList", + "parameterTypes": [] + }, + { + "name": "getMoveTabuSize", + "parameterTypes": [] + }, + { + "name": "getMoveThreadBufferSize", + "parameterTypes": [] + }, + { + "name": "getMoveThreadCount", + "parameterTypes": [] + }, + { + "name": "getNearbyDistanceMeterClass", + "parameterTypes": [] + }, + { + "name": "getNearbySelectionConfig", + "parameterTypes": [] + }, + { + "name": "getNearbySelectionDistributionType", + "parameterTypes": [] + }, + { + "name": "getNodeExplorationType", + "parameterTypes": [] + }, + { + "name": "getOriginEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getOriginSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getOriginSubListSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getOriginValueSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getParabolicDistributionSizeMaximum", + "parameterTypes": [] + }, + { + "name": "getPhaseConfigList", + "parameterTypes": [] + }, + { + "name": "getPickEarlyType", + "parameterTypes": [] + }, + { + "name": "getPillarSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getProbabilityWeightFactoryClass", + "parameterTypes": [] + }, + { + "name": "getRandomFactoryClass", + "parameterTypes": [] + }, + { + "name": "getRandomSeed", + "parameterTypes": [] + }, + { + "name": "getRandomType", + "parameterTypes": [] + }, + { + "name": "getRunnablePartThreadLimit", + "parameterTypes": [] + }, + { + "name": "getScoreCalculationCountLimit", + "parameterTypes": [] + }, + { + "name": "getScoreDirectorFactoryConfig", + "parameterTypes": [] + }, + { + "name": "getScoreDrlList", + "parameterTypes": [] + }, + { + "name": "getSecondaryEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondaryPillarSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondarySubChainSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondarySubListSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondaryValueSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondsSpentLimit", + "parameterTypes": [] + }, + { + "name": "getSelectReversingMoveToo", + "parameterTypes": [] + }, + { + "name": "getSelectedCountLimit", + "parameterTypes": [] + }, + { + "name": "getSelectionOrder", + "parameterTypes": [] + }, + { + "name": "getSelectorProbabilityWeightFactoryClass", + "parameterTypes": [] + }, + { + "name": "getSimulatedAnnealingStartingTemperature", + "parameterTypes": [] + }, + { + "name": "getSolutionClass", + "parameterTypes": [] + }, + { + "name": "getSolutionPartitionerClass", + "parameterTypes": [] + }, + { + "name": "getSolutionPartitionerCustomProperties", + "parameterTypes": [] + }, + { + "name": "getSolverMetricList", + "parameterTypes": [] + }, + { + "name": "getSorterClass", + "parameterTypes": [] + }, + { + "name": "getSorterComparatorClass", + "parameterTypes": [] + }, + { + "name": "getSorterManner", + "parameterTypes": [] + }, + { + "name": "getSorterOrder", + "parameterTypes": [] + }, + { + "name": "getSorterWeightFactoryClass", + "parameterTypes": [] + }, + { + "name": "getSpentLimit", + "parameterTypes": [] + }, + { + "name": "getStepCountLimit", + "parameterTypes": [] + }, + { + "name": "getStepCountingHillClimbingSize", + "parameterTypes": [] + }, + { + "name": "getStepCountingHillClimbingType", + "parameterTypes": [] + }, + { + "name": "getSubChainSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSubListSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSubPillarSequenceComparatorClass", + "parameterTypes": [] + }, + { + "name": "getSubPillarType", + "parameterTypes": [] + }, + { + "name": "getTerminationClass", + "parameterTypes": [] + }, + { + "name": "getTerminationCompositionStyle", + "parameterTypes": [] + }, + { + "name": "getTerminationConfig", + "parameterTypes": [] + }, + { + "name": "getTerminationConfigList", + "parameterTypes": [] + }, + { + "name": "getThreadFactoryClass", + "parameterTypes": [] + }, + { + "name": "getUndoMoveTabuSize", + "parameterTypes": [] + }, + { + "name": "getUnimprovedDaysSpentLimit", + "parameterTypes": [] + }, + { + "name": "getUnimprovedHoursSpentLimit", + "parameterTypes": [] + }, + { + "name": "getUnimprovedMillisecondsSpentLimit", + "parameterTypes": [] + }, + { + "name": "getUnimprovedMinutesSpentLimit", + "parameterTypes": [] + }, + { + "name": "getUnimprovedScoreDifferenceThreshold", + "parameterTypes": [] + }, + { + "name": "getUnimprovedSecondsSpentLimit", + "parameterTypes": [] + }, + { + "name": "getUnimprovedSpentLimit", + "parameterTypes": [] + }, + { + "name": "getUnimprovedStepCountLimit", + "parameterTypes": [] + }, + { + "name": "getValueSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getValueSorterManner", + "parameterTypes": [] + }, + { + "name": "getValueTabuRatio", + "parameterTypes": [] + }, + { + "name": "getValueTabuSize", + "parameterTypes": [] + }, + { + "name": "getVariableName", + "parameterTypes": [] + }, + { + "name": "getVariableNameIncludeList", + "parameterTypes": [] + }, + { + "name": "inherit", + "parameterTypes": [ + "ai.timefold.solver.core.config.AbstractConfig" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] + }, + { + "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, + "fields": [ + { + "name": "ALLOCATE_ENTITY_FROM_QUEUE" + }, + { + "name": "ALLOCATE_FROM_POOL" + }, + { + "name": "ALLOCATE_TO_VALUE_FROM_QUEUE" + }, + { + "name": "CHEAPEST_INSERTION" + }, + { + "name": "FIRST_FIT" + }, + { + "name": "FIRST_FIT_DECREASING" + }, + { + "name": "STRONGEST_FIT" + }, + { + "name": "STRONGEST_FIT_DECREASING" + }, + { + "name": "WEAKEST_FIT" + }, + { + "name": "WEAKEST_FIT_DECREASING" + } + ] + }, + { + "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, + "fields": [ + { + "name": "FIRST_FEASIBLE_SCORE" + }, + { + "name": "FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD" + }, + { + "name": "FIRST_NON_DETERIORATING_SCORE" + }, + { + "name": "NEVER" + } + ] + }, + { + "name": "ai.timefold.solver.core.config.constructionheuristic.placer.EntityPlacerConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getEntityClass", + "parameterTypes": [] + }, + { + "name": "getEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorConfigList", + "parameterTypes": [] + }, + { + "name": "getValueSelectorConfig", + "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, + "fields": [ + { + "name": "BRANCH_AND_BOUND" + }, + { + "name": "BRUTE_FORCE" + } + ] + }, + { + "name": "ai.timefold.solver.core.config.exhaustivesearch.NodeExplorationType", + "allDeclaredFields": true, + "fields": [ + { + "name": "BREADTH_FIRST" + }, + { + "name": "DEPTH_FIRST" + }, + { + "name": "OPTIMISTIC_BOUND_FIRST" + }, + { + "name": "ORIGINAL_ORDER" + }, + { + "name": "SCORE_FIRST" + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.SelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getBetaDistributionAlpha", + "parameterTypes": [] + }, + { + "name": "getBetaDistributionBeta", + "parameterTypes": [] + }, + { + "name": "getBlockDistributionSizeMaximum", + "parameterTypes": [] + }, + { + "name": "getBlockDistributionSizeMinimum", + "parameterTypes": [] + }, + { + "name": "getBlockDistributionSizeRatio", + "parameterTypes": [] + }, + { + "name": "getBlockDistributionUniformDistributionProbability", + "parameterTypes": [] + }, + { + "name": "getCacheType", + "parameterTypes": [] + }, + { + "name": "getDestinationSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getDowncastEntityClass", + "parameterTypes": [] + }, + { + "name": "getEntityClass", + "parameterTypes": [] + }, + { + "name": "getEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getFilterClass", + "parameterTypes": [] + }, + { + "name": "getFixedProbabilityWeight", + "parameterTypes": [] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "getIgnoreEmptyChildIterators", + "parameterTypes": [] + }, + { + "name": "getLinearDistributionSizeMaximum", + "parameterTypes": [] + }, + { + "name": "getMaximumK", + "parameterTypes": [] + }, + { + "name": "getMaximumSubChainSize", + "parameterTypes": [] + }, + { + "name": "getMaximumSubListSize", + "parameterTypes": [] + }, + { + "name": "getMaximumSubPillarSize", + "parameterTypes": [] + }, + { + "name": "getMimicSelectorRef", + "parameterTypes": [] + }, + { + "name": "getMinimumK", + "parameterTypes": [] + }, + { + "name": "getMinimumSubChainSize", + "parameterTypes": [] + }, + { + "name": "getMinimumSubListSize", + "parameterTypes": [] + }, + { + "name": "getMinimumSubPillarSize", + "parameterTypes": [] + }, + { + "name": "getMoveIteratorFactoryClass", + "parameterTypes": [] + }, + { + "name": "getMoveIteratorFactoryCustomProperties", + "parameterTypes": [] + }, + { + "name": "getMoveListFactoryClass", + "parameterTypes": [] + }, + { + "name": "getMoveListFactoryCustomProperties", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorConfigList", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorList", + "parameterTypes": [] + }, + { + "name": "getNearbyDistanceMeterClass", + "parameterTypes": [] + }, + { + "name": "getNearbySelectionConfig", + "parameterTypes": [] + }, + { + "name": "getNearbySelectionDistributionType", + "parameterTypes": [] + }, + { + "name": "getOriginEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getOriginSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getOriginSubListSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getOriginValueSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getParabolicDistributionSizeMaximum", + "parameterTypes": [] + }, + { + "name": "getPillarSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getProbabilityWeightFactoryClass", + "parameterTypes": [] + }, + { + "name": "getSecondaryEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondaryPillarSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondarySubChainSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondarySubListSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondaryValueSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSelectReversingMoveToo", + "parameterTypes": [] + }, + { + "name": "getSelectedCountLimit", + "parameterTypes": [] + }, + { + "name": "getSelectionOrder", + "parameterTypes": [] + }, + { + "name": "getSelectorProbabilityWeightFactoryClass", + "parameterTypes": [] + }, + { + "name": "getSorterClass", + "parameterTypes": [] + }, + { + "name": "getSorterComparatorClass", + "parameterTypes": [] + }, + { + "name": "getSorterManner", + "parameterTypes": [] + }, + { + "name": "getSorterOrder", + "parameterTypes": [] + }, + { + "name": "getSorterWeightFactoryClass", + "parameterTypes": [] + }, + { + "name": "getSubChainSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSubListSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSubPillarSequenceComparatorClass", + "parameterTypes": [] + }, + { + "name": "getSubPillarType", + "parameterTypes": [] + }, + { + "name": "getValueSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getVariableName", + "parameterTypes": [] + }, + { + "name": "getVariableNameIncludeList", + "parameterTypes": [] + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType", + "allDeclaredFields": true, + "fields": [ + { + "name": "JUST_IN_TIME" + }, + { + "name": "PHASE" + }, + { + "name": "SOLVER" + }, + { + "name": "STEP" + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder", + "allDeclaredFields": true, + "fields": [ + { + "name": "INHERIT" + }, + { + "name": "ORIGINAL" + }, + { + "name": "PROBABILISTIC" + }, + { + "name": "RANDOM" + }, + { + "name": "SHUFFLED" + }, + { + "name": "SORTED" + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder", + "allDeclaredFields": true, + "fields": [ + { + "name": "ASCENDING" + }, + { + "name": "DESCENDING" + } + ] + }, + { + "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, + "fields": [ + { + "name": "BETA_DISTRIBUTION" + }, + { + "name": "BLOCK_DISTRIBUTION" + }, + { + "name": "LINEAR_DISTRIBUTION" + }, + { + "name": "PARABOLIC_DISTRIBUTION" + } + ] + }, + { + "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, + "fields": [ + { + "name": "DECREASING_DIFFICULTY" + }, + { + "name": "DECREASING_DIFFICULTY_IF_AVAILABLE" + }, + { + "name": "NONE" + } + ] + }, + { + "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": "getDestinationSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getEntityClass", + "parameterTypes": [] + }, + { + "name": "getEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getIgnoreEmptyChildIterators", + "parameterTypes": [] + }, + { + "name": "getMaximumK", + "parameterTypes": [] + }, + { + "name": "getMaximumSubListSize", + "parameterTypes": [] + }, + { + "name": "getMinimumK", + "parameterTypes": [] + }, + { + "name": "getMinimumSubListSize", + "parameterTypes": [] + }, + { + "name": "getMoveIteratorFactoryClass", + "parameterTypes": [] + }, + { + "name": "getMoveIteratorFactoryCustomProperties", + "parameterTypes": [] + }, + { + "name": "getMoveListFactoryClass", + "parameterTypes": [] + }, + { + "name": "getMoveListFactoryCustomProperties", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorConfigList", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorList", + "parameterTypes": [] + }, + { + "name": "getOriginSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getPillarSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondaryEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondaryPillarSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondarySubChainSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondarySubListSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondaryValueSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSelectReversingMoveToo", + "parameterTypes": [] + }, + { + "name": "getSelectorProbabilityWeightFactoryClass", + "parameterTypes": [] + }, + { + "name": "getSubChainSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSubListSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSubPillarSequenceComparatorClass", + "parameterTypes": [] + }, + { + "name": "getSubPillarType", + "parameterTypes": [] + }, + { + "name": "getValueSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getVariableNameIncludeList", + "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": "getSecondaryPillarSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getValueSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getVariableNameIncludeList", + "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, + "fields": [ + { + "name": "ALL" + }, + { + "name": "NONE" + }, + { + "name": "SEQUENCE" + } + ] + }, + { + "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, + "fields": [ + { + "name": "DECREASING_STRENGTH" + }, + { + "name": "DECREASING_STRENGTH_IF_AVAILABLE" + }, + { + "name": "INCREASING_STRENGTH" + }, + { + "name": "INCREASING_STRENGTH_IF_AVAILABLE" + }, + { + "name": "NONE" + } + ] + }, + { + "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, + "fields": [ + { + "name": "GREAT_DELUGE" + }, + { + "name": "HILL_CLIMBING" + }, + { + "name": "LATE_ACCEPTANCE" + }, + { + "name": "SIMULATED_ANNEALING" + }, + { + "name": "TABU_SEARCH" + }, + { + "name": "VARIABLE_NEIGHBORHOOD_DESCENT" + } + ] + }, + { + "name": "ai.timefold.solver.core.config.localsearch.decider.acceptor.AcceptorType", + "allDeclaredFields": true, + "fields": [ + { + "name": "ENTITY_TABU" + }, + { + "name": "GREAT_DELUGE" + }, + { + "name": "HILL_CLIMBING" + }, + { + "name": "LATE_ACCEPTANCE" + }, + { + "name": "MOVE_TABU" + }, + { + "name": "SIMULATED_ANNEALING" + }, + { + "name": "STEP_COUNTING_HILL_CLIMBING" + }, + { + "name": "UNDO_MOVE_TABU" + }, + { + "name": "VALUE_TABU" + } + ] + }, + { + "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, + "fields": [ + { + "name": "ACCEPTED_MOVE" + }, + { + "name": "EQUAL_OR_IMPROVING_STEP" + }, + { + "name": "IMPROVING_STEP" + }, + { + "name": "SELECTED_MOVE" + }, + { + "name": "STEP" + } + ] + }, + { + "name": "ai.timefold.solver.core.config.localsearch.decider.forager.FinalistPodiumType", + "allDeclaredFields": true, + "fields": [ + { + "name": "HIGHEST_SCORE" + }, + { + "name": "STRATEGIC_OSCILLATION" + }, + { + "name": "STRATEGIC_OSCILLATION_BY_LEVEL" + }, + { + "name": "STRATEGIC_OSCILLATION_BY_LEVEL_ON_BEST_SCORE" + } + ] + }, + { + "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, + "fields": [ + { + "name": "FIRST_BEST_SCORE_IMPROVING" + }, + { + "name": "FIRST_LAST_STEP_SCORE_IMPROVING" + }, + { + "name": "NEVER" + } + ] + }, + { + "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": "getAcceptorConfig", + "parameterTypes": [] + }, + { + "name": "getConstructionHeuristicType", + "parameterTypes": [] + }, + { + "name": "getCustomPhaseCommandClassList", + "parameterTypes": [] + }, + { + "name": "getCustomPhaseCommandList", + "parameterTypes": [] + }, + { + "name": "getCustomProperties", + "parameterTypes": [] + }, + { + "name": "getEntityPlacerConfig", + "parameterTypes": [] + }, + { + "name": "getEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getEntitySorterManner", + "parameterTypes": [] + }, + { + "name": "getExhaustiveSearchType", + "parameterTypes": [] + }, + { + "name": "getForagerConfig", + "parameterTypes": [] + }, + { + "name": "getLocalSearchType", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorConfigList", + "parameterTypes": [] + }, + { + "name": "getNodeExplorationType", + "parameterTypes": [] + }, + { + "name": "getPhaseConfigList", + "parameterTypes": [] + }, + { + "name": "getRunnablePartThreadLimit", + "parameterTypes": [] + }, + { + "name": "getSolutionPartitionerClass", + "parameterTypes": [] + }, + { + "name": "getSolutionPartitionerCustomProperties", + "parameterTypes": [] + }, + { + "name": "getValueSorterManner", + "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, + "fields": [ + { + "name": "FAST_ASSERT" + }, + { + "name": "FULL_ASSERT" + }, + { + "name": "NON_INTRUSIVE_FULL_ASSERT" + }, + { + "name": "NON_REPRODUCIBLE" + }, + { + "name": "REPRODUCIBLE" + }, + { + "name": "TRACKED_FULL_ASSERT" + } + ] + }, + { + "name": "ai.timefold.solver.core.config.solver.SolverConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "copyConfig", + "parameterTypes": [] + }, + { + "name": "inherit", + "parameterTypes": [ + "ai.timefold.solver.core.config.AbstractConfig" + ] + }, + { + "name": "visitReferencedClasses", + "parameterTypes": [ + "java.util.function.Consumer" + ] + } + ] + }, + { + "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, + "fields": [ + { + "name": "BEST_SCORE" + }, + { + "name": "BEST_SOLUTION_MUTATION" + }, + { + "name": "CONSTRAINT_MATCH_TOTAL_BEST_SCORE" + }, + { + "name": "CONSTRAINT_MATCH_TOTAL_STEP_SCORE" + }, + { + "name": "ERROR_COUNT" + }, + { + "name": "MEMORY_USE" + }, + { + "name": "MOVE_COUNT_PER_STEP" + }, + { + "name": "PICKED_MOVE_TYPE_BEST_SCORE_DIFF" + }, + { + "name": "PICKED_MOVE_TYPE_STEP_SCORE_DIFF" + }, + { + "name": "SCORE_CALCULATION_COUNT" + }, + { + "name": "SOLVE_DURATION" + }, + { + "name": "STEP_SCORE" + } + ] + }, + { + "name": "ai.timefold.solver.core.config.solver.random.RandomType", + "allDeclaredFields": true, + "fields": [ + { + "name": "JDK" + }, + { + "name": "MERSENNE_TWISTER" + }, + { + "name": "WELL1024A" + }, + { + "name": "WELL19937A" + }, + { + "name": "WELL19937C" + }, + { + "name": "WELL44497A" + }, + { + "name": "WELL44497B" + }, + { + "name": "WELL512A" + } + ] + }, + { + "name": "ai.timefold.solver.core.config.solver.termination.TerminationCompositionStyle", + "allDeclaredFields": true, + "fields": [ + { + "name": "AND" + }, + { + "name": "OR" + } + ] + }, + { + "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": "ai.timefold.solver.core.impl.solver.DefaultSolverFactory", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "buildSolver", + "parameterTypes": [ + "ai.timefold.solver.core.api.solver.SolverConfigOverride" + ] + }, + { + "name": "close", + "parameterTypes": [] + }, + { + "name": "shutdown", + "parameterTypes": [] + } + ] + }, + { + "name": "ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getSolver", + "parameterTypes": [] + }, + { + "name": "setSolver", + "parameterTypes": [ + "java.util.Map" + ] + } + ] + }, + { + "name": "ai.timefold.solver.test.api.score.stream.ConstraintVerifier" + }, + { + "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": "java.lang.Object", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "getAcceptedCountLimit", + "parameterTypes": [] + }, + { + "name": "getAcceptorConfig", + "parameterTypes": [] + }, + { + "name": "getAcceptorTypeList", + "parameterTypes": [] + }, + { + "name": "getAssertionScoreDirectorFactory", + "parameterTypes": [] + }, + { + "name": "getBestScoreFeasible", + "parameterTypes": [] + }, + { + "name": "getBestScoreLimit", + "parameterTypes": [] + }, + { + "name": "getBetaDistributionAlpha", + "parameterTypes": [] + }, + { + "name": "getBetaDistributionBeta", + "parameterTypes": [] + }, + { + "name": "getBlockDistributionSizeMaximum", + "parameterTypes": [] + }, + { + "name": "getBlockDistributionSizeMinimum", + "parameterTypes": [] + }, + { + "name": "getBlockDistributionSizeRatio", + "parameterTypes": [] + }, + { + "name": "getBlockDistributionUniformDistributionProbability", + "parameterTypes": [] + }, + { + "name": "getBreakTieRandomly", + "parameterTypes": [] + }, + { + "name": "getCacheType", + "parameterTypes": [] + }, + { + "name": "getClassLoader", + "parameterTypes": [] + }, + { + "name": "getConstraintProviderClass", + "parameterTypes": [] + }, + { + "name": "getConstraintProviderCustomProperties", + "parameterTypes": [] + }, + { + "name": "getConstraintStreamImplType", + "parameterTypes": [] + }, + { + "name": "getConstraintStreamShareLambdas", + "parameterTypes": [] + }, + { + "name": "getConstructionHeuristicType", + "parameterTypes": [] + }, + { + "name": "getCustomPhaseCommandClassList", + "parameterTypes": [] + }, + { + "name": "getCustomPhaseCommandList", + "parameterTypes": [] + }, + { + "name": "getCustomProperties", + "parameterTypes": [] + }, + { + "name": "getDaemon", + "parameterTypes": [] + }, + { + "name": "getDaysSpentLimit", + "parameterTypes": [] + }, + { + "name": "getDestinationSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getDomainAccessType", + "parameterTypes": [] + }, + { + "name": "getDowncastEntityClass", + "parameterTypes": [] + }, + { + "name": "getEasyScoreCalculatorClass", + "parameterTypes": [] + }, + { + "name": "getEasyScoreCalculatorCustomProperties", + "parameterTypes": [] + }, + { + "name": "getEntityClass", + "parameterTypes": [] + }, + { + "name": "getEntityClassList", + "parameterTypes": [] + }, + { + "name": "getEntityPlacerConfig", + "parameterTypes": [] + }, + { + "name": "getEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getEntitySorterManner", + "parameterTypes": [] + }, + { + "name": "getEntityTabuRatio", + "parameterTypes": [] + }, + { + "name": "getEntityTabuSize", + "parameterTypes": [] + }, + { + "name": "getEnvironmentMode", + "parameterTypes": [] + }, + { + "name": "getExhaustiveSearchType", + "parameterTypes": [] + }, + { + "name": "getFadingEntityTabuRatio", + "parameterTypes": [] + }, + { + "name": "getFadingEntityTabuSize", + "parameterTypes": [] + }, + { + "name": "getFadingMoveTabuSize", + "parameterTypes": [] + }, + { + "name": "getFadingUndoMoveTabuSize", + "parameterTypes": [] + }, + { + "name": "getFadingValueTabuRatio", + "parameterTypes": [] + }, + { + "name": "getFadingValueTabuSize", + "parameterTypes": [] + }, + { + "name": "getFilterClass", + "parameterTypes": [] + }, + { + "name": "getFinalistPodiumType", + "parameterTypes": [] + }, + { + "name": "getFixedProbabilityWeight", + "parameterTypes": [] + }, + { + "name": "getForagerConfig", + "parameterTypes": [] + }, + { + "name": "getGizmoMemberAccessorMap", + "parameterTypes": [] + }, + { + "name": "getGizmoSolutionClonerMap", + "parameterTypes": [] + }, + { + "name": "getGreatDelugeWaterLevelIncrementRatio", + "parameterTypes": [] + }, + { + "name": "getGreatDelugeWaterLevelIncrementScore", + "parameterTypes": [] + }, + { + "name": "getHoursSpentLimit", + "parameterTypes": [] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "getIgnoreEmptyChildIterators", + "parameterTypes": [] + }, + { + "name": "getIncrementalScoreCalculatorClass", + "parameterTypes": [] + }, + { + "name": "getIncrementalScoreCalculatorCustomProperties", + "parameterTypes": [] + }, + { + "name": "getInitializingScoreTrend", + "parameterTypes": [] + }, + { + "name": "getLateAcceptanceSize", + "parameterTypes": [] + }, + { + "name": "getLinearDistributionSizeMaximum", + "parameterTypes": [] + }, + { + "name": "getLocalSearchType", + "parameterTypes": [] + }, + { + "name": "getMaximumK", + "parameterTypes": [] + }, + { + "name": "getMaximumSubChainSize", + "parameterTypes": [] + }, + { + "name": "getMaximumSubListSize", + "parameterTypes": [] + }, + { + "name": "getMaximumSubPillarSize", + "parameterTypes": [] + }, + { + "name": "getMillisecondsSpentLimit", + "parameterTypes": [] + }, + { + "name": "getMimicSelectorRef", + "parameterTypes": [] + }, + { + "name": "getMinimumK", + "parameterTypes": [] + }, + { + "name": "getMinimumSubChainSize", + "parameterTypes": [] + }, + { + "name": "getMinimumSubListSize", + "parameterTypes": [] + }, + { + "name": "getMinimumSubPillarSize", + "parameterTypes": [] + }, + { + "name": "getMinutesSpentLimit", + "parameterTypes": [] + }, + { + "name": "getMonitoringConfig", + "parameterTypes": [] + }, + { + "name": "getMoveIteratorFactoryClass", + "parameterTypes": [] + }, + { + "name": "getMoveIteratorFactoryCustomProperties", + "parameterTypes": [] + }, + { + "name": "getMoveListFactoryClass", + "parameterTypes": [] + }, + { + "name": "getMoveListFactoryCustomProperties", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorConfigList", + "parameterTypes": [] + }, + { + "name": "getMoveSelectorList", + "parameterTypes": [] + }, + { + "name": "getMoveTabuSize", + "parameterTypes": [] + }, + { + "name": "getMoveThreadBufferSize", + "parameterTypes": [] + }, + { + "name": "getMoveThreadCount", + "parameterTypes": [] + }, + { + "name": "getNearbyDistanceMeterClass", + "parameterTypes": [] + }, + { + "name": "getNearbySelectionConfig", + "parameterTypes": [] + }, + { + "name": "getNearbySelectionDistributionType", + "parameterTypes": [] + }, + { + "name": "getNodeExplorationType", + "parameterTypes": [] + }, + { + "name": "getOriginEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getOriginSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getOriginSubListSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getOriginValueSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getParabolicDistributionSizeMaximum", + "parameterTypes": [] + }, + { + "name": "getPhaseConfigList", + "parameterTypes": [] + }, + { + "name": "getPickEarlyType", + "parameterTypes": [] + }, + { + "name": "getPillarSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getProbabilityWeightFactoryClass", + "parameterTypes": [] + }, + { + "name": "getRandomFactoryClass", + "parameterTypes": [] + }, + { + "name": "getRandomSeed", + "parameterTypes": [] + }, + { + "name": "getRandomType", + "parameterTypes": [] + }, + { + "name": "getRunnablePartThreadLimit", + "parameterTypes": [] + }, + { + "name": "getScoreCalculationCountLimit", + "parameterTypes": [] + }, + { + "name": "getScoreDirectorFactoryConfig", + "parameterTypes": [] + }, + { + "name": "getScoreDrlList", + "parameterTypes": [] + }, + { + "name": "getSecondaryEntitySelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondaryPillarSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondarySubChainSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondarySubListSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondaryValueSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSecondsSpentLimit", + "parameterTypes": [] + }, + { + "name": "getSelectReversingMoveToo", + "parameterTypes": [] + }, + { + "name": "getSelectedCountLimit", + "parameterTypes": [] + }, + { + "name": "getSelectionOrder", + "parameterTypes": [] + }, + { + "name": "getSelectorProbabilityWeightFactoryClass", + "parameterTypes": [] + }, + { + "name": "getSimulatedAnnealingStartingTemperature", + "parameterTypes": [] + }, + { + "name": "getSolutionClass", + "parameterTypes": [] + }, + { + "name": "getSolutionPartitionerClass", + "parameterTypes": [] + }, + { + "name": "getSolutionPartitionerCustomProperties", + "parameterTypes": [] + }, + { + "name": "getSolverMetricList", + "parameterTypes": [] + }, + { + "name": "getSorterClass", + "parameterTypes": [] + }, + { + "name": "getSorterComparatorClass", + "parameterTypes": [] + }, + { + "name": "getSorterManner", + "parameterTypes": [] + }, + { + "name": "getSorterOrder", + "parameterTypes": [] + }, + { + "name": "getSorterWeightFactoryClass", + "parameterTypes": [] + }, + { + "name": "getSpentLimit", + "parameterTypes": [] + }, + { + "name": "getStepCountLimit", + "parameterTypes": [] + }, + { + "name": "getStepCountingHillClimbingSize", + "parameterTypes": [] + }, + { + "name": "getStepCountingHillClimbingType", + "parameterTypes": [] + }, + { + "name": "getSubChainSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSubListSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getSubPillarSequenceComparatorClass", + "parameterTypes": [] + }, + { + "name": "getSubPillarType", + "parameterTypes": [] + }, + { + "name": "getTerminationClass", + "parameterTypes": [] + }, + { + "name": "getTerminationCompositionStyle", + "parameterTypes": [] + }, + { + "name": "getTerminationConfig", + "parameterTypes": [] + }, + { + "name": "getTerminationConfigList", + "parameterTypes": [] + }, + { + "name": "getThreadFactoryClass", + "parameterTypes": [] + }, + { + "name": "getUndoMoveTabuSize", + "parameterTypes": [] + }, + { + "name": "getUnimprovedDaysSpentLimit", + "parameterTypes": [] + }, + { + "name": "getUnimprovedHoursSpentLimit", + "parameterTypes": [] + }, + { + "name": "getUnimprovedMillisecondsSpentLimit", + "parameterTypes": [] + }, + { + "name": "getUnimprovedMinutesSpentLimit", + "parameterTypes": [] + }, + { + "name": "getUnimprovedScoreDifferenceThreshold", + "parameterTypes": [] + }, + { + "name": "getUnimprovedSecondsSpentLimit", + "parameterTypes": [] + }, + { + "name": "getUnimprovedSpentLimit", + "parameterTypes": [] + }, + { + "name": "getUnimprovedStepCountLimit", + "parameterTypes": [] + }, + { + "name": "getValueSelectorConfig", + "parameterTypes": [] + }, + { + "name": "getValueSorterManner", + "parameterTypes": [] + }, + { + "name": "getValueTabuRatio", + "parameterTypes": [] + }, + { + "name": "getValueTabuSize", + "parameterTypes": [] + }, + { + "name": "getVariableName", + "parameterTypes": [] + }, + { + "name": "getVariableNameIncludeList", + "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 From c6ddac7c2c7014198d37b12b27bdee80b62508a8 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 15 Feb 2024 13:19:51 -0500 Subject: [PATCH 10/12] chore: Reduce the number of items in reflect-config.json The no-args default constructor is not included in "queryAllDeclaredMethods". --- .../reflect-config.json | 3254 +++-------------- 1 file changed, 527 insertions(+), 2727 deletions(-) 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 index cd57aeeefa..34a25fa039 100644 --- 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 @@ -45,2867 +45,667 @@ ] }, { - "name": "ai.timefold.solver.benchmark.api.PlannerBenchmarkFactory" + "name": "ai.timefold.solver.core.api.domain.common.DomainAccessType", + "allDeclaredFields": true }, { - "name": "ai.timefold.solver.core.api.domain.common.DomainAccessType", + "name": "ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType", + "allDeclaredFields": true + }, + { + "name": "ai.timefold.solver.core.config.AbstractConfig", "allDeclaredFields": true, - "fields": [ - { - "name": "GIZMO" - }, + "queryAllDeclaredMethods": true + }, + { + "name": "ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "REFLECTION" + "name": "", + "parameterTypes": [] } ] }, { - "name": "ai.timefold.solver.core.api.domain.entity.PlanningEntity", - "queryAllDeclaredMethods": true - }, - { - "name": "ai.timefold.solver.core.api.domain.solution.PlanningSolution", - "queryAllDeclaredMethods": true + "name": "ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicType", + "allDeclaredFields": true }, { - "name": "ai.timefold.solver.core.api.score.Score", - "queryAllDeclaredMethods": true + "name": "ai.timefold.solver.core.config.constructionheuristic.decider.forager.ConstructionHeuristicForagerConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] }, { - "name": "ai.timefold.solver.core.api.score.ScoreManager", - "queryAllDeclaredMethods": true + "name": "ai.timefold.solver.core.config.constructionheuristic.decider.forager.ConstructionHeuristicPickEarlyType", + "allDeclaredFields": true }, { - "name": "ai.timefold.solver.core.api.score.buildin.simple.SimpleScore", + "name": "ai.timefold.solver.core.config.constructionheuristic.placer.EntityPlacerConfig", "allDeclaredFields": true, "queryAllDeclaredMethods": true, - "queryAllDeclaredConstructors": true + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] }, { - "name": "ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType", + "name": "ai.timefold.solver.core.config.constructionheuristic.placer.PooledEntityPlacerConfig", "allDeclaredFields": true, - "fields": [ - { - "name": "BAVET" - }, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "DROOLS" + "name": "", + "parameterTypes": [] } ] }, { - "name": "ai.timefold.solver.core.api.solver.SolutionManager", - "queryAllDeclaredMethods": true + "name": "ai.timefold.solver.core.config.constructionheuristic.placer.QueuedEntityPlacerConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] }, { - "name": "ai.timefold.solver.core.api.solver.SolverFactory", + "name": "ai.timefold.solver.core.config.constructionheuristic.placer.QueuedValuePlacerConfig", + "allDeclaredFields": true, "queryAllDeclaredMethods": true, - "queryAllPublicMethods": true, "methods": [ { - "name": "buildSolver", + "name": "", "parameterTypes": [] } ] }, { - "name": "ai.timefold.solver.core.api.solver.SolverManager", + "name": "ai.timefold.solver.core.config.exhaustivesearch.ExhaustiveSearchPhaseConfig", + "allDeclaredFields": true, "queryAllDeclaredMethods": true, "methods": [ { - "name": "close", + "name": "", "parameterTypes": [] } ] }, { - "name": "ai.timefold.solver.core.config.AbstractConfig", + "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": "getAcceptedCountLimit", + "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": "getAcceptorConfig", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.list.DestinationSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getAcceptorTypeList", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.list.SubListSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getAssertionScoreDirectorFactory", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getBestScoreFeasible", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.composite.CartesianProductMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getBestScoreLimit", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getBetaDistributionAlpha", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveIteratorFactoryConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getBetaDistributionBeta", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveListFactoryConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getBlockDistributionSizeMaximum", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.AbstractPillarMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getBlockDistributionSizeMinimum", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getBlockDistributionSizeRatio", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getBlockDistributionUniformDistributionProbability", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getBreakTieRandomly", + "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": "getCacheType", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getClassLoader", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainSwapMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getConstraintProviderClass", + "name": "", "parameterTypes": [] - }, - { - "name": "getConstraintProviderCustomProperties", - "parameterTypes": [] - }, - { - "name": "getConstraintStreamImplType", - "parameterTypes": [] - }, - { - "name": "getConstraintStreamShareLambdas", - "parameterTypes": [] - }, - { - "name": "getConstructionHeuristicType", - "parameterTypes": [] - }, - { - "name": "getCustomPhaseCommandClassList", - "parameterTypes": [] - }, - { - "name": "getCustomPhaseCommandList", - "parameterTypes": [] - }, - { - "name": "getCustomProperties", - "parameterTypes": [] - }, - { - "name": "getDaemon", - "parameterTypes": [] - }, - { - "name": "getDaysSpentLimit", - "parameterTypes": [] - }, - { - "name": "getDestinationSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getDomainAccessType", - "parameterTypes": [] - }, - { - "name": "getDowncastEntityClass", - "parameterTypes": [] - }, - { - "name": "getEasyScoreCalculatorClass", - "parameterTypes": [] - }, - { - "name": "getEasyScoreCalculatorCustomProperties", - "parameterTypes": [] - }, - { - "name": "getEntityClass", - "parameterTypes": [] - }, - { - "name": "getEntityClassList", - "parameterTypes": [] - }, - { - "name": "getEntityPlacerConfig", - "parameterTypes": [] - }, - { - "name": "getEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getEntitySorterManner", - "parameterTypes": [] - }, - { - "name": "getEntityTabuRatio", - "parameterTypes": [] - }, - { - "name": "getEntityTabuSize", - "parameterTypes": [] - }, - { - "name": "getEnvironmentMode", - "parameterTypes": [] - }, - { - "name": "getExhaustiveSearchType", - "parameterTypes": [] - }, - { - "name": "getFadingEntityTabuRatio", - "parameterTypes": [] - }, - { - "name": "getFadingEntityTabuSize", - "parameterTypes": [] - }, - { - "name": "getFadingMoveTabuSize", - "parameterTypes": [] - }, - { - "name": "getFadingUndoMoveTabuSize", - "parameterTypes": [] - }, - { - "name": "getFadingValueTabuRatio", - "parameterTypes": [] - }, - { - "name": "getFadingValueTabuSize", - "parameterTypes": [] - }, - { - "name": "getFilterClass", - "parameterTypes": [] - }, - { - "name": "getFinalistPodiumType", - "parameterTypes": [] - }, - { - "name": "getFixedProbabilityWeight", - "parameterTypes": [] - }, - { - "name": "getForagerConfig", - "parameterTypes": [] - }, - { - "name": "getGizmoMemberAccessorMap", - "parameterTypes": [] - }, - { - "name": "getGizmoSolutionClonerMap", - "parameterTypes": [] - }, - { - "name": "getGreatDelugeWaterLevelIncrementRatio", - "parameterTypes": [] - }, - { - "name": "getGreatDelugeWaterLevelIncrementScore", - "parameterTypes": [] - }, - { - "name": "getHoursSpentLimit", - "parameterTypes": [] - }, - { - "name": "getId", - "parameterTypes": [] - }, - { - "name": "getIgnoreEmptyChildIterators", - "parameterTypes": [] - }, - { - "name": "getIncrementalScoreCalculatorClass", - "parameterTypes": [] - }, - { - "name": "getIncrementalScoreCalculatorCustomProperties", - "parameterTypes": [] - }, - { - "name": "getInitializingScoreTrend", - "parameterTypes": [] - }, - { - "name": "getLateAcceptanceSize", - "parameterTypes": [] - }, - { - "name": "getLinearDistributionSizeMaximum", - "parameterTypes": [] - }, - { - "name": "getLocalSearchType", - "parameterTypes": [] - }, - { - "name": "getMaximumK", - "parameterTypes": [] - }, - { - "name": "getMaximumSubChainSize", - "parameterTypes": [] - }, - { - "name": "getMaximumSubListSize", - "parameterTypes": [] - }, - { - "name": "getMaximumSubPillarSize", - "parameterTypes": [] - }, - { - "name": "getMillisecondsSpentLimit", - "parameterTypes": [] - }, - { - "name": "getMimicSelectorRef", - "parameterTypes": [] - }, - { - "name": "getMinimumK", - "parameterTypes": [] - }, - { - "name": "getMinimumSubChainSize", - "parameterTypes": [] - }, - { - "name": "getMinimumSubListSize", - "parameterTypes": [] - }, - { - "name": "getMinimumSubPillarSize", - "parameterTypes": [] - }, - { - "name": "getMinutesSpentLimit", - "parameterTypes": [] - }, - { - "name": "getMonitoringConfig", - "parameterTypes": [] - }, - { - "name": "getMoveIteratorFactoryClass", - "parameterTypes": [] - }, - { - "name": "getMoveIteratorFactoryCustomProperties", - "parameterTypes": [] - }, - { - "name": "getMoveListFactoryClass", - "parameterTypes": [] - }, - { - "name": "getMoveListFactoryCustomProperties", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorConfigList", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorList", - "parameterTypes": [] - }, - { - "name": "getMoveTabuSize", - "parameterTypes": [] - }, - { - "name": "getMoveThreadBufferSize", - "parameterTypes": [] - }, - { - "name": "getMoveThreadCount", - "parameterTypes": [] - }, - { - "name": "getNearbyDistanceMeterClass", - "parameterTypes": [] - }, - { - "name": "getNearbySelectionConfig", - "parameterTypes": [] - }, - { - "name": "getNearbySelectionDistributionType", - "parameterTypes": [] - }, - { - "name": "getNodeExplorationType", - "parameterTypes": [] - }, - { - "name": "getOriginEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getOriginSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getOriginSubListSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getOriginValueSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getParabolicDistributionSizeMaximum", - "parameterTypes": [] - }, - { - "name": "getPhaseConfigList", - "parameterTypes": [] - }, - { - "name": "getPickEarlyType", - "parameterTypes": [] - }, - { - "name": "getPillarSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getProbabilityWeightFactoryClass", - "parameterTypes": [] - }, - { - "name": "getRandomFactoryClass", - "parameterTypes": [] - }, - { - "name": "getRandomSeed", - "parameterTypes": [] - }, - { - "name": "getRandomType", - "parameterTypes": [] - }, - { - "name": "getRunnablePartThreadLimit", - "parameterTypes": [] - }, - { - "name": "getScoreCalculationCountLimit", - "parameterTypes": [] - }, - { - "name": "getScoreDirectorFactoryConfig", - "parameterTypes": [] - }, - { - "name": "getScoreDrlList", - "parameterTypes": [] - }, - { - "name": "getSecondaryEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondaryPillarSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondarySubChainSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondarySubListSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondaryValueSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondsSpentLimit", - "parameterTypes": [] - }, - { - "name": "getSelectReversingMoveToo", - "parameterTypes": [] - }, - { - "name": "getSelectedCountLimit", - "parameterTypes": [] - }, - { - "name": "getSelectionOrder", - "parameterTypes": [] - }, - { - "name": "getSelectorProbabilityWeightFactoryClass", - "parameterTypes": [] - }, - { - "name": "getSimulatedAnnealingStartingTemperature", - "parameterTypes": [] - }, - { - "name": "getSolutionClass", - "parameterTypes": [] - }, - { - "name": "getSolutionPartitionerClass", - "parameterTypes": [] - }, - { - "name": "getSolutionPartitionerCustomProperties", - "parameterTypes": [] - }, - { - "name": "getSolverMetricList", - "parameterTypes": [] - }, - { - "name": "getSorterClass", - "parameterTypes": [] - }, - { - "name": "getSorterComparatorClass", - "parameterTypes": [] - }, - { - "name": "getSorterManner", - "parameterTypes": [] - }, - { - "name": "getSorterOrder", - "parameterTypes": [] - }, - { - "name": "getSorterWeightFactoryClass", - "parameterTypes": [] - }, - { - "name": "getSpentLimit", - "parameterTypes": [] - }, - { - "name": "getStepCountLimit", - "parameterTypes": [] - }, - { - "name": "getStepCountingHillClimbingSize", - "parameterTypes": [] - }, - { - "name": "getStepCountingHillClimbingType", - "parameterTypes": [] - }, - { - "name": "getSubChainSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSubListSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSubPillarSequenceComparatorClass", - "parameterTypes": [] - }, - { - "name": "getSubPillarType", - "parameterTypes": [] - }, - { - "name": "getTerminationClass", - "parameterTypes": [] - }, - { - "name": "getTerminationCompositionStyle", - "parameterTypes": [] - }, - { - "name": "getTerminationConfig", - "parameterTypes": [] - }, - { - "name": "getTerminationConfigList", - "parameterTypes": [] - }, - { - "name": "getThreadFactoryClass", - "parameterTypes": [] - }, - { - "name": "getUndoMoveTabuSize", - "parameterTypes": [] - }, - { - "name": "getUnimprovedDaysSpentLimit", - "parameterTypes": [] - }, - { - "name": "getUnimprovedHoursSpentLimit", - "parameterTypes": [] - }, - { - "name": "getUnimprovedMillisecondsSpentLimit", - "parameterTypes": [] - }, - { - "name": "getUnimprovedMinutesSpentLimit", - "parameterTypes": [] - }, - { - "name": "getUnimprovedScoreDifferenceThreshold", - "parameterTypes": [] - }, - { - "name": "getUnimprovedSecondsSpentLimit", - "parameterTypes": [] - }, - { - "name": "getUnimprovedSpentLimit", - "parameterTypes": [] - }, - { - "name": "getUnimprovedStepCountLimit", - "parameterTypes": [] - }, - { - "name": "getValueSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getValueSorterManner", - "parameterTypes": [] - }, - { - "name": "getValueTabuRatio", - "parameterTypes": [] - }, - { - "name": "getValueTabuSize", - "parameterTypes": [] - }, - { - "name": "getVariableName", - "parameterTypes": [] - }, - { - "name": "getVariableNameIncludeList", - "parameterTypes": [] - }, - { - "name": "inherit", - "parameterTypes": [ - "ai.timefold.solver.core.config.AbstractConfig" - ] - }, - { - "name": "toString", - "parameterTypes": [] - } - ] - }, - { - "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, - "fields": [ - { - "name": "ALLOCATE_ENTITY_FROM_QUEUE" - }, - { - "name": "ALLOCATE_FROM_POOL" - }, - { - "name": "ALLOCATE_TO_VALUE_FROM_QUEUE" - }, - { - "name": "CHEAPEST_INSERTION" - }, - { - "name": "FIRST_FIT" - }, - { - "name": "FIRST_FIT_DECREASING" - }, - { - "name": "STRONGEST_FIT" - }, - { - "name": "STRONGEST_FIT_DECREASING" - }, - { - "name": "WEAKEST_FIT" - }, - { - "name": "WEAKEST_FIT_DECREASING" - } - ] - }, - { - "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, - "fields": [ - { - "name": "FIRST_FEASIBLE_SCORE" - }, - { - "name": "FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD" - }, - { - "name": "FIRST_NON_DETERIORATING_SCORE" - }, - { - "name": "NEVER" - } - ] - }, - { - "name": "ai.timefold.solver.core.config.constructionheuristic.placer.EntityPlacerConfig", - "allDeclaredFields": true, - "queryAllDeclaredMethods": true, - "methods": [ - { - "name": "", - "parameterTypes": [] - }, - { - "name": "getEntityClass", - "parameterTypes": [] - }, - { - "name": "getEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorConfigList", - "parameterTypes": [] - }, - { - "name": "getValueSelectorConfig", - "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, - "fields": [ - { - "name": "BRANCH_AND_BOUND" - }, - { - "name": "BRUTE_FORCE" - } - ] - }, - { - "name": "ai.timefold.solver.core.config.exhaustivesearch.NodeExplorationType", - "allDeclaredFields": true, - "fields": [ - { - "name": "BREADTH_FIRST" - }, - { - "name": "DEPTH_FIRST" - }, - { - "name": "OPTIMISTIC_BOUND_FIRST" - }, - { - "name": "ORIGINAL_ORDER" - }, - { - "name": "SCORE_FIRST" - } - ] - }, - { - "name": "ai.timefold.solver.core.config.heuristic.selector.SelectorConfig", - "allDeclaredFields": true, - "queryAllDeclaredMethods": true, - "methods": [ - { - "name": "", - "parameterTypes": [] - }, - { - "name": "getBetaDistributionAlpha", - "parameterTypes": [] - }, - { - "name": "getBetaDistributionBeta", - "parameterTypes": [] - }, - { - "name": "getBlockDistributionSizeMaximum", - "parameterTypes": [] - }, - { - "name": "getBlockDistributionSizeMinimum", - "parameterTypes": [] - }, - { - "name": "getBlockDistributionSizeRatio", - "parameterTypes": [] - }, - { - "name": "getBlockDistributionUniformDistributionProbability", - "parameterTypes": [] - }, - { - "name": "getCacheType", - "parameterTypes": [] - }, - { - "name": "getDestinationSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getDowncastEntityClass", - "parameterTypes": [] - }, - { - "name": "getEntityClass", - "parameterTypes": [] - }, - { - "name": "getEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getFilterClass", - "parameterTypes": [] - }, - { - "name": "getFixedProbabilityWeight", - "parameterTypes": [] - }, - { - "name": "getId", - "parameterTypes": [] - }, - { - "name": "getIgnoreEmptyChildIterators", - "parameterTypes": [] - }, - { - "name": "getLinearDistributionSizeMaximum", - "parameterTypes": [] - }, - { - "name": "getMaximumK", - "parameterTypes": [] - }, - { - "name": "getMaximumSubChainSize", - "parameterTypes": [] - }, - { - "name": "getMaximumSubListSize", - "parameterTypes": [] - }, - { - "name": "getMaximumSubPillarSize", - "parameterTypes": [] - }, - { - "name": "getMimicSelectorRef", - "parameterTypes": [] - }, - { - "name": "getMinimumK", - "parameterTypes": [] - }, - { - "name": "getMinimumSubChainSize", - "parameterTypes": [] - }, - { - "name": "getMinimumSubListSize", - "parameterTypes": [] - }, - { - "name": "getMinimumSubPillarSize", - "parameterTypes": [] - }, - { - "name": "getMoveIteratorFactoryClass", - "parameterTypes": [] - }, - { - "name": "getMoveIteratorFactoryCustomProperties", - "parameterTypes": [] - }, - { - "name": "getMoveListFactoryClass", - "parameterTypes": [] - }, - { - "name": "getMoveListFactoryCustomProperties", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorConfigList", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorList", - "parameterTypes": [] - }, - { - "name": "getNearbyDistanceMeterClass", - "parameterTypes": [] - }, - { - "name": "getNearbySelectionConfig", - "parameterTypes": [] - }, - { - "name": "getNearbySelectionDistributionType", - "parameterTypes": [] - }, - { - "name": "getOriginEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getOriginSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getOriginSubListSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getOriginValueSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getParabolicDistributionSizeMaximum", - "parameterTypes": [] - }, - { - "name": "getPillarSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getProbabilityWeightFactoryClass", - "parameterTypes": [] - }, - { - "name": "getSecondaryEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondaryPillarSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondarySubChainSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondarySubListSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondaryValueSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSelectReversingMoveToo", - "parameterTypes": [] - }, - { - "name": "getSelectedCountLimit", - "parameterTypes": [] - }, - { - "name": "getSelectionOrder", - "parameterTypes": [] - }, - { - "name": "getSelectorProbabilityWeightFactoryClass", - "parameterTypes": [] - }, - { - "name": "getSorterClass", - "parameterTypes": [] - }, - { - "name": "getSorterComparatorClass", - "parameterTypes": [] - }, - { - "name": "getSorterManner", - "parameterTypes": [] - }, - { - "name": "getSorterOrder", - "parameterTypes": [] - }, - { - "name": "getSorterWeightFactoryClass", - "parameterTypes": [] - }, - { - "name": "getSubChainSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSubListSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSubPillarSequenceComparatorClass", - "parameterTypes": [] - }, - { - "name": "getSubPillarType", - "parameterTypes": [] - }, - { - "name": "getValueSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getVariableName", - "parameterTypes": [] - }, - { - "name": "getVariableNameIncludeList", - "parameterTypes": [] - } - ] - }, - { - "name": "ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType", - "allDeclaredFields": true, - "fields": [ - { - "name": "JUST_IN_TIME" - }, - { - "name": "PHASE" - }, - { - "name": "SOLVER" - }, - { - "name": "STEP" - } - ] - }, - { - "name": "ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder", - "allDeclaredFields": true, - "fields": [ - { - "name": "INHERIT" - }, - { - "name": "ORIGINAL" - }, - { - "name": "PROBABILISTIC" - }, - { - "name": "RANDOM" - }, - { - "name": "SHUFFLED" - }, - { - "name": "SORTED" - } - ] - }, - { - "name": "ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder", - "allDeclaredFields": true, - "fields": [ - { - "name": "ASCENDING" - }, - { - "name": "DESCENDING" - } - ] - }, - { - "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, - "fields": [ - { - "name": "BETA_DISTRIBUTION" - }, - { - "name": "BLOCK_DISTRIBUTION" - }, - { - "name": "LINEAR_DISTRIBUTION" - }, - { - "name": "PARABOLIC_DISTRIBUTION" - } - ] - }, - { - "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, - "fields": [ - { - "name": "DECREASING_DIFFICULTY" - }, - { - "name": "DECREASING_DIFFICULTY_IF_AVAILABLE" - }, - { - "name": "NONE" - } - ] - }, - { - "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": "getDestinationSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getEntityClass", - "parameterTypes": [] - }, - { - "name": "getEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getIgnoreEmptyChildIterators", - "parameterTypes": [] - }, - { - "name": "getMaximumK", - "parameterTypes": [] - }, - { - "name": "getMaximumSubListSize", - "parameterTypes": [] - }, - { - "name": "getMinimumK", - "parameterTypes": [] - }, - { - "name": "getMinimumSubListSize", - "parameterTypes": [] - }, - { - "name": "getMoveIteratorFactoryClass", - "parameterTypes": [] - }, - { - "name": "getMoveIteratorFactoryCustomProperties", - "parameterTypes": [] - }, - { - "name": "getMoveListFactoryClass", - "parameterTypes": [] - }, - { - "name": "getMoveListFactoryCustomProperties", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorConfigList", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorList", - "parameterTypes": [] - }, - { - "name": "getOriginSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getPillarSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondaryEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondaryPillarSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondarySubChainSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondarySubListSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondaryValueSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSelectReversingMoveToo", - "parameterTypes": [] - }, - { - "name": "getSelectorProbabilityWeightFactoryClass", - "parameterTypes": [] - }, - { - "name": "getSubChainSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSubListSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSubPillarSequenceComparatorClass", - "parameterTypes": [] - }, - { - "name": "getSubPillarType", - "parameterTypes": [] - }, - { - "name": "getValueSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getVariableNameIncludeList", - "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": "getSecondaryPillarSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getValueSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getVariableNameIncludeList", - "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, - "fields": [ - { - "name": "ALL" - }, - { - "name": "NONE" - }, - { - "name": "SEQUENCE" - } - ] - }, - { - "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, - "fields": [ - { - "name": "DECREASING_STRENGTH" - }, - { - "name": "DECREASING_STRENGTH_IF_AVAILABLE" - }, - { - "name": "INCREASING_STRENGTH" - }, - { - "name": "INCREASING_STRENGTH_IF_AVAILABLE" - }, - { - "name": "NONE" - } - ] - }, - { - "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, - "fields": [ - { - "name": "GREAT_DELUGE" - }, - { - "name": "HILL_CLIMBING" - }, - { - "name": "LATE_ACCEPTANCE" - }, - { - "name": "SIMULATED_ANNEALING" - }, - { - "name": "TABU_SEARCH" - }, - { - "name": "VARIABLE_NEIGHBORHOOD_DESCENT" - } - ] - }, - { - "name": "ai.timefold.solver.core.config.localsearch.decider.acceptor.AcceptorType", - "allDeclaredFields": true, - "fields": [ - { - "name": "ENTITY_TABU" - }, - { - "name": "GREAT_DELUGE" - }, - { - "name": "HILL_CLIMBING" - }, - { - "name": "LATE_ACCEPTANCE" - }, - { - "name": "MOVE_TABU" - }, - { - "name": "SIMULATED_ANNEALING" - }, - { - "name": "STEP_COUNTING_HILL_CLIMBING" - }, - { - "name": "UNDO_MOVE_TABU" - }, - { - "name": "VALUE_TABU" - } - ] - }, - { - "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, - "fields": [ - { - "name": "ACCEPTED_MOVE" - }, - { - "name": "EQUAL_OR_IMPROVING_STEP" - }, - { - "name": "IMPROVING_STEP" - }, - { - "name": "SELECTED_MOVE" - }, - { - "name": "STEP" - } - ] - }, - { - "name": "ai.timefold.solver.core.config.localsearch.decider.forager.FinalistPodiumType", - "allDeclaredFields": true, - "fields": [ - { - "name": "HIGHEST_SCORE" - }, - { - "name": "STRATEGIC_OSCILLATION" - }, - { - "name": "STRATEGIC_OSCILLATION_BY_LEVEL" - }, - { - "name": "STRATEGIC_OSCILLATION_BY_LEVEL_ON_BEST_SCORE" - } - ] - }, - { - "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, - "fields": [ - { - "name": "FIRST_BEST_SCORE_IMPROVING" - }, - { - "name": "FIRST_LAST_STEP_SCORE_IMPROVING" - }, - { - "name": "NEVER" - } - ] - }, - { - "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": "getAcceptorConfig", - "parameterTypes": [] - }, - { - "name": "getConstructionHeuristicType", - "parameterTypes": [] - }, - { - "name": "getCustomPhaseCommandClassList", - "parameterTypes": [] - }, - { - "name": "getCustomPhaseCommandList", - "parameterTypes": [] - }, - { - "name": "getCustomProperties", - "parameterTypes": [] - }, - { - "name": "getEntityPlacerConfig", - "parameterTypes": [] - }, - { - "name": "getEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getEntitySorterManner", - "parameterTypes": [] - }, - { - "name": "getExhaustiveSearchType", - "parameterTypes": [] - }, - { - "name": "getForagerConfig", - "parameterTypes": [] - }, - { - "name": "getLocalSearchType", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorConfigList", - "parameterTypes": [] - }, - { - "name": "getNodeExplorationType", - "parameterTypes": [] - }, - { - "name": "getPhaseConfigList", - "parameterTypes": [] - }, - { - "name": "getRunnablePartThreadLimit", - "parameterTypes": [] - }, - { - "name": "getSolutionPartitionerClass", - "parameterTypes": [] - }, - { - "name": "getSolutionPartitionerCustomProperties", - "parameterTypes": [] - }, - { - "name": "getValueSorterManner", - "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, - "fields": [ - { - "name": "FAST_ASSERT" - }, - { - "name": "FULL_ASSERT" - }, - { - "name": "NON_INTRUSIVE_FULL_ASSERT" - }, - { - "name": "NON_REPRODUCIBLE" - }, - { - "name": "REPRODUCIBLE" - }, - { - "name": "TRACKED_FULL_ASSERT" - } - ] - }, - { - "name": "ai.timefold.solver.core.config.solver.SolverConfig", - "allDeclaredFields": true, - "queryAllDeclaredMethods": true, - "methods": [ - { - "name": "", - "parameterTypes": [] - }, - { - "name": "copyConfig", - "parameterTypes": [] - }, - { - "name": "inherit", - "parameterTypes": [ - "ai.timefold.solver.core.config.AbstractConfig" - ] - }, - { - "name": "visitReferencedClasses", - "parameterTypes": [ - "java.util.function.Consumer" - ] - } - ] - }, - { - "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, - "fields": [ - { - "name": "BEST_SCORE" - }, - { - "name": "BEST_SOLUTION_MUTATION" - }, - { - "name": "CONSTRAINT_MATCH_TOTAL_BEST_SCORE" - }, - { - "name": "CONSTRAINT_MATCH_TOTAL_STEP_SCORE" - }, - { - "name": "ERROR_COUNT" - }, - { - "name": "MEMORY_USE" - }, - { - "name": "MOVE_COUNT_PER_STEP" - }, - { - "name": "PICKED_MOVE_TYPE_BEST_SCORE_DIFF" - }, - { - "name": "PICKED_MOVE_TYPE_STEP_SCORE_DIFF" - }, - { - "name": "SCORE_CALCULATION_COUNT" - }, - { - "name": "SOLVE_DURATION" - }, - { - "name": "STEP_SCORE" - } - ] - }, - { - "name": "ai.timefold.solver.core.config.solver.random.RandomType", - "allDeclaredFields": true, - "fields": [ - { - "name": "JDK" - }, - { - "name": "MERSENNE_TWISTER" - }, - { - "name": "WELL1024A" - }, - { - "name": "WELL19937A" - }, - { - "name": "WELL19937C" - }, - { - "name": "WELL44497A" - }, - { - "name": "WELL44497B" - }, - { - "name": "WELL512A" - } - ] - }, - { - "name": "ai.timefold.solver.core.config.solver.termination.TerminationCompositionStyle", - "allDeclaredFields": true, - "fields": [ - { - "name": "AND" - }, - { - "name": "OR" - } - ] - }, - { - "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": "ai.timefold.solver.core.impl.solver.DefaultSolverFactory", - "allDeclaredFields": true, - "queryAllDeclaredMethods": true, - "methods": [ - { - "name": "buildSolver", - "parameterTypes": [ - "ai.timefold.solver.core.api.solver.SolverConfigOverride" - ] - }, - { - "name": "close", - "parameterTypes": [] - }, - { - "name": "shutdown", - "parameterTypes": [] - } - ] - }, - { - "name": "ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties", - "allDeclaredFields": true, - "queryAllDeclaredMethods": true, - "queryAllDeclaredConstructors": true, - "methods": [ - { - "name": "", - "parameterTypes": [] - }, - { - "name": "getSolver", - "parameterTypes": [] - }, - { - "name": "setSolver", - "parameterTypes": [ - "java.util.Map" - ] - } - ] - }, - { - "name": "ai.timefold.solver.test.api.score.stream.ConstraintVerifier" - }, - { - "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": "java.lang.Object", - "allDeclaredFields": true, - "queryAllDeclaredMethods": true, - "methods": [ - { - "name": "getAcceptedCountLimit", - "parameterTypes": [] - }, - { - "name": "getAcceptorConfig", - "parameterTypes": [] - }, - { - "name": "getAcceptorTypeList", - "parameterTypes": [] - }, - { - "name": "getAssertionScoreDirectorFactory", - "parameterTypes": [] - }, - { - "name": "getBestScoreFeasible", - "parameterTypes": [] - }, - { - "name": "getBestScoreLimit", - "parameterTypes": [] - }, - { - "name": "getBetaDistributionAlpha", - "parameterTypes": [] - }, - { - "name": "getBetaDistributionBeta", - "parameterTypes": [] - }, - { - "name": "getBlockDistributionSizeMaximum", - "parameterTypes": [] - }, - { - "name": "getBlockDistributionSizeMinimum", - "parameterTypes": [] - }, - { - "name": "getBlockDistributionSizeRatio", - "parameterTypes": [] - }, - { - "name": "getBlockDistributionUniformDistributionProbability", - "parameterTypes": [] - }, - { - "name": "getBreakTieRandomly", - "parameterTypes": [] - }, - { - "name": "getCacheType", - "parameterTypes": [] - }, - { - "name": "getClassLoader", - "parameterTypes": [] - }, - { - "name": "getConstraintProviderClass", - "parameterTypes": [] - }, - { - "name": "getConstraintProviderCustomProperties", - "parameterTypes": [] - }, - { - "name": "getConstraintStreamImplType", - "parameterTypes": [] - }, - { - "name": "getConstraintStreamShareLambdas", - "parameterTypes": [] - }, - { - "name": "getConstructionHeuristicType", - "parameterTypes": [] - }, - { - "name": "getCustomPhaseCommandClassList", - "parameterTypes": [] - }, - { - "name": "getCustomPhaseCommandList", - "parameterTypes": [] - }, - { - "name": "getCustomProperties", - "parameterTypes": [] - }, - { - "name": "getDaemon", - "parameterTypes": [] - }, - { - "name": "getDaysSpentLimit", - "parameterTypes": [] - }, - { - "name": "getDestinationSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getDomainAccessType", - "parameterTypes": [] - }, - { - "name": "getDowncastEntityClass", - "parameterTypes": [] - }, - { - "name": "getEasyScoreCalculatorClass", - "parameterTypes": [] - }, - { - "name": "getEasyScoreCalculatorCustomProperties", - "parameterTypes": [] - }, - { - "name": "getEntityClass", - "parameterTypes": [] - }, - { - "name": "getEntityClassList", - "parameterTypes": [] - }, - { - "name": "getEntityPlacerConfig", - "parameterTypes": [] - }, - { - "name": "getEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getEntitySorterManner", - "parameterTypes": [] - }, - { - "name": "getEntityTabuRatio", - "parameterTypes": [] - }, - { - "name": "getEntityTabuSize", - "parameterTypes": [] - }, - { - "name": "getEnvironmentMode", - "parameterTypes": [] - }, - { - "name": "getExhaustiveSearchType", - "parameterTypes": [] - }, - { - "name": "getFadingEntityTabuRatio", - "parameterTypes": [] - }, - { - "name": "getFadingEntityTabuSize", - "parameterTypes": [] - }, - { - "name": "getFadingMoveTabuSize", - "parameterTypes": [] - }, - { - "name": "getFadingUndoMoveTabuSize", - "parameterTypes": [] - }, - { - "name": "getFadingValueTabuRatio", - "parameterTypes": [] - }, - { - "name": "getFadingValueTabuSize", - "parameterTypes": [] - }, - { - "name": "getFilterClass", - "parameterTypes": [] - }, - { - "name": "getFinalistPodiumType", - "parameterTypes": [] - }, - { - "name": "getFixedProbabilityWeight", - "parameterTypes": [] - }, - { - "name": "getForagerConfig", - "parameterTypes": [] - }, - { - "name": "getGizmoMemberAccessorMap", - "parameterTypes": [] - }, - { - "name": "getGizmoSolutionClonerMap", - "parameterTypes": [] - }, - { - "name": "getGreatDelugeWaterLevelIncrementRatio", - "parameterTypes": [] - }, - { - "name": "getGreatDelugeWaterLevelIncrementScore", - "parameterTypes": [] - }, - { - "name": "getHoursSpentLimit", - "parameterTypes": [] - }, - { - "name": "getId", - "parameterTypes": [] - }, - { - "name": "getIgnoreEmptyChildIterators", - "parameterTypes": [] - }, - { - "name": "getIncrementalScoreCalculatorClass", - "parameterTypes": [] - }, - { - "name": "getIncrementalScoreCalculatorCustomProperties", - "parameterTypes": [] - }, - { - "name": "getInitializingScoreTrend", - "parameterTypes": [] - }, - { - "name": "getLateAcceptanceSize", - "parameterTypes": [] - }, - { - "name": "getLinearDistributionSizeMaximum", - "parameterTypes": [] - }, - { - "name": "getLocalSearchType", - "parameterTypes": [] - }, - { - "name": "getMaximumK", - "parameterTypes": [] - }, - { - "name": "getMaximumSubChainSize", - "parameterTypes": [] - }, - { - "name": "getMaximumSubListSize", - "parameterTypes": [] - }, - { - "name": "getMaximumSubPillarSize", - "parameterTypes": [] - }, - { - "name": "getMillisecondsSpentLimit", - "parameterTypes": [] - }, - { - "name": "getMimicSelectorRef", - "parameterTypes": [] - }, - { - "name": "getMinimumK", - "parameterTypes": [] - }, - { - "name": "getMinimumSubChainSize", - "parameterTypes": [] - }, - { - "name": "getMinimumSubListSize", - "parameterTypes": [] - }, - { - "name": "getMinimumSubPillarSize", - "parameterTypes": [] - }, - { - "name": "getMinutesSpentLimit", - "parameterTypes": [] - }, - { - "name": "getMonitoringConfig", - "parameterTypes": [] - }, - { - "name": "getMoveIteratorFactoryClass", - "parameterTypes": [] - }, - { - "name": "getMoveIteratorFactoryCustomProperties", - "parameterTypes": [] - }, - { - "name": "getMoveListFactoryClass", - "parameterTypes": [] - }, - { - "name": "getMoveListFactoryCustomProperties", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorConfigList", - "parameterTypes": [] - }, - { - "name": "getMoveSelectorList", - "parameterTypes": [] - }, - { - "name": "getMoveTabuSize", - "parameterTypes": [] - }, - { - "name": "getMoveThreadBufferSize", - "parameterTypes": [] - }, - { - "name": "getMoveThreadCount", - "parameterTypes": [] - }, - { - "name": "getNearbyDistanceMeterClass", - "parameterTypes": [] - }, - { - "name": "getNearbySelectionConfig", - "parameterTypes": [] - }, - { - "name": "getNearbySelectionDistributionType", - "parameterTypes": [] - }, - { - "name": "getNodeExplorationType", - "parameterTypes": [] - }, - { - "name": "getOriginEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getOriginSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getOriginSubListSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getOriginValueSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getParabolicDistributionSizeMaximum", - "parameterTypes": [] - }, - { - "name": "getPhaseConfigList", - "parameterTypes": [] - }, - { - "name": "getPickEarlyType", - "parameterTypes": [] - }, - { - "name": "getPillarSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getProbabilityWeightFactoryClass", - "parameterTypes": [] - }, - { - "name": "getRandomFactoryClass", - "parameterTypes": [] - }, - { - "name": "getRandomSeed", - "parameterTypes": [] - }, - { - "name": "getRandomType", - "parameterTypes": [] - }, - { - "name": "getRunnablePartThreadLimit", - "parameterTypes": [] - }, - { - "name": "getScoreCalculationCountLimit", - "parameterTypes": [] - }, - { - "name": "getScoreDirectorFactoryConfig", - "parameterTypes": [] - }, - { - "name": "getScoreDrlList", - "parameterTypes": [] - }, - { - "name": "getSecondaryEntitySelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondaryPillarSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondarySubChainSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondarySubListSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondaryValueSelectorConfig", - "parameterTypes": [] - }, - { - "name": "getSecondsSpentLimit", - "parameterTypes": [] - }, - { - "name": "getSelectReversingMoveToo", - "parameterTypes": [] - }, - { - "name": "getSelectedCountLimit", - "parameterTypes": [] - }, - { - "name": "getSelectionOrder", - "parameterTypes": [] - }, - { - "name": "getSelectorProbabilityWeightFactoryClass", - "parameterTypes": [] - }, - { - "name": "getSimulatedAnnealingStartingTemperature", - "parameterTypes": [] - }, - { - "name": "getSolutionClass", - "parameterTypes": [] - }, - { - "name": "getSolutionPartitionerClass", - "parameterTypes": [] - }, - { - "name": "getSolutionPartitionerCustomProperties", - "parameterTypes": [] - }, - { - "name": "getSolverMetricList", - "parameterTypes": [] - }, - { - "name": "getSorterClass", - "parameterTypes": [] - }, - { - "name": "getSorterComparatorClass", - "parameterTypes": [] - }, - { - "name": "getSorterManner", - "parameterTypes": [] - }, - { - "name": "getSorterOrder", - "parameterTypes": [] - }, - { - "name": "getSorterWeightFactoryClass", - "parameterTypes": [] - }, - { - "name": "getSpentLimit", - "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.TailChainSwapMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getStepCountLimit", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getStepCountingHillClimbingSize", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getStepCountingHillClimbingType", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListChangeMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getSubChainSelectorConfig", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListSwapMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getSubListSelectorConfig", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.move.generic.list.kopt.KOptListMoveSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getSubPillarSequenceComparatorClass", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getSubPillarType", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner", + "allDeclaredFields": true, + "methods": [ { - "name": "getTerminationClass", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.heuristic.selector.value.chained.SubChainSelectorConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getTerminationCompositionStyle", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getTerminationConfig", + "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": "getTerminationConfigList", + "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": "getThreadFactoryClass", + "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": "getUndoMoveTabuSize", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.phase.NoChangePhaseConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getUnimprovedDaysSpentLimit", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.phase.PhaseConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getUnimprovedHoursSpentLimit", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.phase.custom.CustomPhaseConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getUnimprovedMillisecondsSpentLimit", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getUnimprovedMinutesSpentLimit", + "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": "getUnimprovedScoreDifferenceThreshold", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.config.solver.monitoring.MonitoringConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getUnimprovedSecondsSpentLimit", + "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": "getUnimprovedSpentLimit", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbCustomPropertiesAdapter", + "methods": [ { - "name": "getUnimprovedStepCountLimit", + "name": "", "parameterTypes": [] - }, + } + ] + }, + { + "name": "ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbCustomPropertiesAdapter$JaxbAdaptedMap", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbCustomPropertiesAdapter$JaxbAdaptedMapEntry", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "jakarta.xml.bind.Binder" + }, + { + "name": "jakarta.xml.bind.annotation.XmlAccessorType", + "queryAllDeclaredMethods": true + }, + { + "name": "jakarta.xml.bind.annotation.XmlElement", + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getValueSelectorConfig", + "name": "type", "parameterTypes": [] - }, + } + ] + }, + { + "name": "jakarta.xml.bind.annotation.XmlElements", + "queryAllDeclaredMethods": true + }, + { + "name": "jakarta.xml.bind.annotation.XmlEnum", + "methods": [ { - "name": "getValueSorterManner", + "name": "value", "parameterTypes": [] - }, + } + ] + }, + { + "name": "jakarta.xml.bind.annotation.XmlRootElement", + "queryAllDeclaredMethods": true + }, + { + "name": "jakarta.xml.bind.annotation.XmlSeeAlso", + "methods": [ { - "name": "getValueTabuRatio", + "name": "value", "parameterTypes": [] - }, + } + ] + }, + { + "name": "jakarta.xml.bind.annotation.XmlTransient", + "queryAllDeclaredMethods": true + }, + { + "name": "jakarta.xml.bind.annotation.XmlType", + "queryAllDeclaredMethods": true, + "methods": [ { - "name": "getValueTabuSize", + "name": "factoryClass", "parameterTypes": [] - }, + } + ] + }, + { + "name": "jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter", + "methods": [ { - "name": "getVariableName", + "name": "type", "parameterTypes": [] }, { - "name": "getVariableNameIncludeList", + "name": "value", "parameterTypes": [] } ] From 86cbe2c4adae55a215ab4898858e25a27dfad95f Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 15 Feb 2024 16:57:17 -0500 Subject: [PATCH 11/12] chore: Simplify reflect-config.json and add a large spring native XML test --- .../reflect-config.json | 16 +- .../resource-config.json | 9 + ...foldSolverTestResourceIntegrationTest.java | 24 + .../native-image/resource-config.json | 9 + .../src/test/resources/solver-full.xml | 1151 +++++++++++++++++ 5 files changed, 1207 insertions(+), 2 deletions(-) create mode 100644 spring-integration/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/ai.timefold.solver/timefold-solver-spring-boot-autoconfigure/resource-config.json create mode 100644 spring-integration/spring-boot-integration-test/src/test/resources/META-INF/native-image/resource-config.json create mode 100644 spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml 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 index 34a25fa039..12579138e6 100644 --- 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 @@ -633,12 +633,24 @@ { "name": "ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbCustomPropertiesAdapter$JaxbAdaptedMap", "allDeclaredFields": true, - "queryAllDeclaredMethods": true + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] }, { "name": "ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbCustomPropertiesAdapter$JaxbAdaptedMapEntry", "allDeclaredFields": true, - "queryAllDeclaredMethods": true + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] }, { "name": "jakarta.xml.bind.Binder" 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-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 index 991f322b2b..dfd09da0e2 100644 --- 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 @@ -1,14 +1,22 @@ 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) @@ -17,6 +25,9 @@ public class TimefoldSolverTestResourceIntegrationTest { @LocalServerPort String port; + @Value("classpath:solver-full.xml") + Resource exampleSolverConfigXml; + @Test void testSolve() { WebTestClient client = WebTestClient.bindToServer() @@ -45,4 +56,17 @@ void testSolve() { .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 From 6aeee096c29934fa5cce6b6ca68cb8b55ce133c0 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Fri, 16 Feb 2024 12:04:31 -0500 Subject: [PATCH 12/12] chore: Address SonarCloud issues, change score classes -> score calculator classes in Spring error message --- .../TimefoldSolverAotContribution.java | 2 +- .../TimefoldSolverAutoConfiguration.java | 54 ++++++++++++------- .../TimefoldSolverBeanFactory.java | 15 +++--- .../TimefoldSolverAutoConfigurationTest.java | 22 ++++---- ...erMultipleSolverAutoConfigurationTest.java | 12 ++--- ...foldSolverTestResourceIntegrationTest.java | 2 +- 6 files changed, 60 insertions(+), 47 deletions(-) diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java index 47a3e74f8e..e88b533c10 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAotContribution.java @@ -21,7 +21,7 @@ public TimefoldSolverAotContribution(Map solverConfigMap) * Register a type for reflection, allowing introspection * of its members at runtime in a native build. */ - private void registerType(ReflectionHints reflectionHints, Class type) { + private static void registerType(ReflectionHints reflectionHints, Class type) { reflectionHints.registerType(type, MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS, diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java index c87b48b92a..24284b5aae 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java @@ -89,7 +89,6 @@ public class TimefoldSolverAutoConfiguration }; private ApplicationContext context; private ClassLoader beanClassLoader; - private Environment environment; private TimefoldProperties timefoldProperties; protected TimefoldSolverAutoConfiguration() { @@ -110,7 +109,6 @@ public void setEnvironment(Environment environment) { // postProcessBeanFactory runs before creating any bean, but we need TimefoldProperties. // Therefore, we use the Environment to load the properties BindResult result = Binder.get(environment).bind("timefold", TimefoldProperties.class); - this.environment = environment; this.timefoldProperties = result.orElseGet(TimefoldProperties::new); } @@ -157,8 +155,6 @@ public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableL @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { Map solverConfigMap = getSolverConfigMap(); - BindResult result = Binder.get(environment).bind("timefold", TimefoldProperties.class); - TimefoldProperties timefoldProperties = result.orElseGet(TimefoldProperties::new); SolverConfigIO solverConfigIO = new SolverConfigIO(); registry.registerBeanDefinition(TimefoldSolverAotFactory.class.getName(), new RootBeanDefinition(TimefoldSolverAotFactory.class)); @@ -280,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)); @@ -311,7 +308,7 @@ static void applyTerminationProperties(SolverConfig solverConfig, TerminationPro } } - private void assertNoMemberAnnotationWithoutClassAnnotation(IncludeAbstractClassesEntityScanner entityScanner) { + private static void assertNoMemberAnnotationWithoutClassAnnotation(IncludeAbstractClassesEntityScanner entityScanner) { List> timefoldFieldAnnotationList = entityScanner.findClassesWithAnnotation(PLANNING_ENTITY_FIELD_ANNOTATIONS); List> entityList = entityScanner.findEntityClassList(); @@ -328,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 @@ -382,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. @@ -395,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); @@ -403,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( @@ -416,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(", ")), @@ -433,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) @@ -471,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() @@ -511,7 +524,8 @@ Some solver configs (%s) don't specify a %s score class, yet there are multiple } } - private void assertEmptyInstances(IncludeAbstractClassesEntityScanner entityScanner, Class clazz, + private static void assertEmptyInstances(IncludeAbstractClassesEntityScanner entityScanner, + Class clazz, String errorMessage) { try { Collection> classInstanceCollection = entityScanner.scan(clazz); @@ -523,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) 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 index ec05bf8695..ff5411f0c8 100644 --- 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 @@ -83,7 +83,7 @@ public TimefoldSolverBannerBean getBanner() { public SolverFactory getSolverFactory() { failInjectionWithMultipleSolvers(SolverFactory.class.getName()); SolverConfig solverConfig = context.getBean(SolverConfig.class); - if (solverConfig == null || solverConfig.getSolutionClass() == null) { + if (solverConfig.getSolutionClass() == null) { return null; } return SolverFactory.create(solverConfig); @@ -109,12 +109,12 @@ public SolverManager solverManage @Lazy @ConditionalOnMissingBean @Deprecated(forRemoval = true) + /** + * @deprecated Use {@link SolutionManager} instead. + */ public > ScoreManager scoreManager() { failInjectionWithMultipleSolvers(ScoreManager.class.getName()); - SolverFactory solverFactory = context.getBean(SolverFactory.class); - if (solverFactory == null) { - return null; - } + SolverFactory solverFactory = context.getBean(SolverFactory.class); return ScoreManager.create(solverFactory); } @@ -123,10 +123,7 @@ public > ScoreManager @ConditionalOnMissingBean public > SolutionManager solutionManager() { failInjectionWithMultipleSolvers(SolutionManager.class.getName()); - SolverFactory solverFactory = context.getBean(SolverFactory.class); - if (solverFactory == null) { - return null; - } + SolverFactory solverFactory = context.getBean(SolverFactory.class); return SolutionManager.create(solverFactory); } diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfigurationTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfigurationTest.java index ccaab0848e..85a1bb1c73 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfigurationTest.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; @@ -474,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(); }); } @@ -492,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(); }); } @@ -512,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(); }); } @@ -656,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."); } @@ -668,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."); } @@ -680,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."); } @@ -693,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"); @@ -707,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."); } @@ -720,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/TimefoldSolverMultipleSolverAutoConfigurationTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverMultipleSolverAutoConfigurationTest.java index 1777f87bb9..60613d7691 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverMultipleSolverAutoConfigurationTest.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverMultipleSolverAutoConfigurationTest.java @@ -558,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."); @@ -573,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."); @@ -588,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."); @@ -605,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."); @@ -622,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."); @@ -639,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/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 index dfd09da0e2..50460b95f6 100644 --- 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 @@ -20,7 +20,7 @@ import org.springframework.test.web.reactive.server.WebTestClient; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class TimefoldSolverTestResourceIntegrationTest { +class TimefoldSolverTestResourceIntegrationTest { @LocalServerPort String port;