Skip to content

Commit bb652ea

Browse files
chore: Move Pojo serialization logic to it own class and add tests for it
1 parent 1d247ed commit bb652ea

File tree

3 files changed

+1000
-222
lines changed

3 files changed

+1000
-222
lines changed

spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAotContribution.java

Lines changed: 3 additions & 222 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package ai.timefold.solver.spring.boot.autoconfigure;
22

3-
import java.lang.reflect.Array;
4-
import java.lang.reflect.Field;
5-
import java.time.Duration;
63
import java.util.ArrayList;
74
import java.util.HashMap;
85
import java.util.HashSet;
9-
import java.util.IdentityHashMap;
106
import java.util.List;
117
import java.util.Map;
128
import java.util.Set;
@@ -19,8 +15,8 @@
1915
import ai.timefold.solver.core.config.solver.SolverManagerConfig;
2016
import ai.timefold.solver.spring.boot.autoconfigure.config.SolverManagerProperties;
2117
import ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties;
18+
import ai.timefold.solver.spring.boot.autoconfigure.util.PojoInliner;
2219

23-
import org.apache.commons.text.StringEscapeUtils;
2420
import org.springframework.aot.generate.DefaultMethodReference;
2521
import org.springframework.aot.generate.GeneratedClass;
2622
import org.springframework.aot.generate.GenerationContext;
@@ -82,221 +78,6 @@ private void registerType(ReflectionHints reflectionHints, Class<?> type) {
8278
// and surrounding it by double quotes.
8379
// - $T: Format as a fully qualified type, which allows you to use
8480
// classes without importing them.
85-
86-
/**
87-
* Serializes a Pojo to code that uses its no-args constructor
88-
* and setters to create the object.
89-
*
90-
* @param pojo The object to be serialized.
91-
* @param initializerCode The code block builder of the initializer
92-
* @param complexPojoToIdentifier A map that stores objects already recorded
93-
* @return A string that can be used in a {@link CodeBlock.Builder} to access the object
94-
*/
95-
public static String pojoToCode(Object pojo,
96-
CodeBlock.Builder initializerCode,
97-
Map<Object, String> complexPojoToIdentifier) {
98-
// First, check for primitives
99-
if (pojo == null) {
100-
return "null";
101-
}
102-
if (pojo instanceof Boolean value) {
103-
return value.toString();
104-
}
105-
if (pojo instanceof Byte value) {
106-
return value.toString();
107-
}
108-
if (pojo instanceof Character value) {
109-
return "\\u" + Integer.toHexString(value | 0x10000).substring(1);
110-
}
111-
if (pojo instanceof Short value) {
112-
return value.toString();
113-
}
114-
if (pojo instanceof Integer value) {
115-
return value.toString();
116-
}
117-
if (pojo instanceof Long value) {
118-
// Add long suffix to number string
119-
return value + "L";
120-
}
121-
if (pojo instanceof Float value) {
122-
// Add float suffix to number string
123-
return value + "f";
124-
}
125-
if (pojo instanceof Double value) {
126-
// Add double suffix to number string
127-
return value + "d";
128-
}
129-
130-
// Check for builtin classes
131-
if (pojo instanceof String value) {
132-
return "\"" + StringEscapeUtils.escapeJava(value) + "\"";
133-
}
134-
if (pojo instanceof Class<?> value) {
135-
return value.getName() + ".class";
136-
}
137-
if (pojo instanceof ClassLoader) {
138-
// We don't support serializing ClassLoaders, so replace it
139-
// with the context class loader
140-
return "Thread.currentThread().getContextClassLoader()";
141-
}
142-
if (pojo instanceof Duration value) {
143-
return Duration.class.getName() + ".ofNanos(" + value.toNanos() + "L)";
144-
}
145-
if (pojo.getClass().isEnum()) {
146-
// Use field access to read the enum
147-
Class<?> enumClass = pojo.getClass();
148-
Enum<?> pojoEnum = (Enum<?>) pojo;
149-
return enumClass.getName() + "." + pojoEnum.name();
150-
}
151-
return complexPojoToCode(pojo, initializerCode, complexPojoToIdentifier);
152-
}
153-
154-
/**
155-
* Return a string that can be used in a {@link CodeBlock.Builder} to access a complex object
156-
*
157-
* @param pojo The object to be accessed
158-
* @param complexPojoToIdentifier A Map from complex POJOs to their key in the map.
159-
* @return A string that can be used in a {@link CodeBlock.Builder} to access the object.
160-
*/
161-
private static String getComplexPojo(Object pojo, Map<Object, String> complexPojoToIdentifier) {
162-
return "((" + pojo.getClass().getName() + ") " + COMPLEX_POJO_MAP_FIELD_NAME + ".get(\""
163-
+ complexPojoToIdentifier.get(pojo) + "\"))";
164-
}
165-
166-
/**
167-
* Serializes collections and complex POJOs to code
168-
*/
169-
private static String complexPojoToCode(Object pojo, CodeBlock.Builder initializerCode,
170-
Map<Object, String> complexPojoToIdentifier) {
171-
// If we already serialized the object, we should just return
172-
// the code string
173-
if (complexPojoToIdentifier.containsKey(pojo)) {
174-
return getComplexPojo(pojo, complexPojoToIdentifier);
175-
}
176-
// Object is not serialized yet
177-
// Create a new variable to store its value when setting its fields
178-
String newIdentifier = "$obj" + complexPojoToIdentifier.size();
179-
complexPojoToIdentifier.put(pojo, newIdentifier);
180-
initializerCode.add("\n$T $L;", pojo.getClass(), newIdentifier);
181-
182-
// First, check if it is a collection type
183-
if (pojo.getClass().isArray()) {
184-
return arrayToCode(newIdentifier, pojo, initializerCode, complexPojoToIdentifier);
185-
}
186-
if (pojo instanceof List<?> value) {
187-
return listToCode(newIdentifier, value, initializerCode, complexPojoToIdentifier);
188-
}
189-
if (pojo instanceof Set<?> value) {
190-
return setToCode(newIdentifier, value, initializerCode, complexPojoToIdentifier);
191-
}
192-
if (pojo instanceof Map<?, ?> value) {
193-
return mapToCode(newIdentifier, value, initializerCode, complexPojoToIdentifier);
194-
}
195-
196-
// Not a collection type, so serialize by creating a new instance and settings its fields
197-
initializerCode.add("\n$L = new $T();", newIdentifier, pojo.getClass());
198-
initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier);
199-
setComplexPojoFields(pojo.getClass(), newIdentifier, pojo, initializerCode, complexPojoToIdentifier);
200-
return getComplexPojo(pojo, complexPojoToIdentifier);
201-
}
202-
203-
private static String arrayToCode(String newIdentifier, Object array, CodeBlock.Builder initializerCode,
204-
Map<Object, String> complexPojoToIdentifier) {
205-
// Get the length of the array
206-
int length = Array.getLength(array);
207-
208-
// Create a new array from the component type with the given length
209-
initializerCode.add("\n$L = new $T[$L];", newIdentifier, array.getClass().getComponentType(), Integer.toString(length));
210-
initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier);
211-
for (int i = 0; i < length; i++) {
212-
// Set the elements of the array
213-
initializerCode.add("\n$L[$L] = $L;",
214-
newIdentifier,
215-
Integer.toString(i),
216-
pojoToCode(Array.get(array, i), initializerCode, complexPojoToIdentifier));
217-
}
218-
return getComplexPojo(array, complexPojoToIdentifier);
219-
}
220-
221-
private static String listToCode(String newIdentifier, List<?> list, CodeBlock.Builder initializerCode,
222-
Map<Object, String> complexPojoToIdentifier) {
223-
// Create an ArrayList
224-
initializerCode.add("\n$L = new $T($L);", newIdentifier, ArrayList.class, Integer.toString(list.size()));
225-
initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier);
226-
for (Object item : list) {
227-
// Add each item of the list to the ArrayList
228-
initializerCode.add("\n$L.add($L);",
229-
newIdentifier,
230-
pojoToCode(item, initializerCode, complexPojoToIdentifier));
231-
}
232-
return getComplexPojo(list, complexPojoToIdentifier);
233-
}
234-
235-
private static String setToCode(String newIdentifier, Set<?> set, CodeBlock.Builder initializerCode,
236-
Map<Object, String> complexPojoToIdentifier) {
237-
// Create a new HashSet
238-
initializerCode.add("\n$L = new $T($L);", newIdentifier, HashSet.class, Integer.toString(set.size()));
239-
initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier);
240-
for (Object item : set) {
241-
// Add each item of the set to the HashSet
242-
initializerCode.add("\n$L.add($L);",
243-
newIdentifier,
244-
pojoToCode(item, initializerCode, complexPojoToIdentifier));
245-
}
246-
return getComplexPojo(set, complexPojoToIdentifier);
247-
}
248-
249-
private static String mapToCode(String newIdentifier, Map<?, ?> map, CodeBlock.Builder initializerCode,
250-
Map<Object, String> complexPojoToIdentifier) {
251-
// Create a HashMap
252-
initializerCode.add("\n$L = new $T($L);", newIdentifier, HashMap.class, Integer.toString(map.size()));
253-
initializerCode.add("\n$L.put($S, $L);", COMPLEX_POJO_MAP_FIELD_NAME, newIdentifier, newIdentifier);
254-
for (Map.Entry<?, ?> entry : map.entrySet()) {
255-
// Put each entry of the map into the HashMap
256-
initializerCode.add("\n$L.put($L,$L);",
257-
newIdentifier,
258-
pojoToCode(entry.getKey(), initializerCode, complexPojoToIdentifier),
259-
pojoToCode(entry.getValue(), initializerCode, complexPojoToIdentifier));
260-
}
261-
return getComplexPojo(map, complexPojoToIdentifier);
262-
}
263-
264-
/**
265-
* Sets the fields of pojo declared in pojoClass and all its superclasses.
266-
*
267-
* @param pojoClass A class assignable to pojo containing some of its fields.
268-
* @param identifier The name of the variable storing the serialized pojo.
269-
* @param pojo The object being serialized.
270-
* @param initializerCode The {@link CodeBlock.Builder} to use to generate code in the initializer.
271-
* @param complexPojoToIdentifier A map from complex POJOs to their variable name.
272-
*/
273-
private static void setComplexPojoFields(Class<?> pojoClass, String identifier, Object pojo,
274-
CodeBlock.Builder initializerCode, Map<Object, String> complexPojoToIdentifier) {
275-
if (pojoClass == Object.class) {
276-
// We are the top-level, no more fields to set
277-
return;
278-
}
279-
for (Field field : pojo.getClass().getDeclaredFields()) {
280-
if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
281-
// We do not want to write static fields
282-
continue;
283-
}
284-
// Set the field accessible so we can read its value
285-
field.setAccessible(true);
286-
try {
287-
// Convert the field value to code, and call the setter
288-
// corresponding to the field with the serialized field value.
289-
initializerCode.add("\n$L.set$L$L($L);", identifier,
290-
Character.toUpperCase(field.getName().charAt(0)),
291-
field.getName().substring(1),
292-
pojoToCode(field.get(pojo), initializerCode, complexPojoToIdentifier));
293-
} catch (IllegalAccessException e) {
294-
throw new RuntimeException(e);
295-
}
296-
}
297-
setComplexPojoFields(pojoClass.getSuperclass(), identifier, pojo, initializerCode, complexPojoToIdentifier);
298-
}
299-
30081
@Override
30182
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
30283
var reflectionHints = generationContext.getRuntimeHints().reflection();
@@ -319,15 +100,15 @@ public void applyTo(GenerationContext generationContext, BeanFactoryInitializati
319100
// Create a generated class to hold all the solver configs
320101
GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("timefold-aot",
321102
builder -> {
103+
PojoInliner pojoInliner = new PojoInliner();
322104
builder.addField(Map.class, "solverConfigMap", Modifier.STATIC);
323105
builder.addField(Map.class, COMPLEX_POJO_MAP_FIELD_NAME, Modifier.STATIC);
324106

325107
// Handwrite the SolverConfig map in the initializer
326108
CodeBlock.Builder staticInitializer = CodeBlock.builder();
327-
Map<Object, String> complexPojoToIdentifier = new IdentityHashMap<>();
328109
staticInitializer.add("$L = new $T();", COMPLEX_POJO_MAP_FIELD_NAME, HashMap.class);
329110
staticInitializer.add("\nsolverConfigMap = $L;",
330-
complexPojoToCode(solverConfigMap, staticInitializer, complexPojoToIdentifier));
111+
pojoInliner.getInlinedPojo(solverConfigMap, staticInitializer));
331112
builder.addStaticBlock(staticInitializer.build());
332113

333114
// getSolverConfig fetches the SolverConfig with the given name from the map

0 commit comments

Comments
 (0)