upgrade to springboot 4

This commit is contained in:
Lucio Lelii 2026-05-05 19:22:53 +02:00
parent 66f6fff376
commit 8a0f9d5c8d
43 changed files with 341 additions and 126 deletions

View File

@ -16,4 +16,4 @@
# under the License.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/4.0.0-rc-5/apache-maven-4.0.0-rc-5-bin.zip

30
pom.xml
View File

@ -75,15 +75,20 @@
</dependency>
<!-- https://mvnrepository.com/artifact/com.kjetland/mbknor-jackson-jsonschema -->
<!-- Jackson 3 native JSON Schema generator (replaces mbknor-jackson-jsonschema) -->
<dependency>
<groupId>com.kjetland</groupId>
<artifactId>mbknor-jackson-jsonschema_2.12</artifactId>
<version>1.0.39</version>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-module-jackson</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- oppure jjwt-gson -->
<artifactId>jjwt-gson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
@ -105,10 +110,7 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
@ -186,6 +188,16 @@
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:${settings.localRepository}/net/bytebuddy/byte-buddy-agent/${byte-buddy.version}/byte-buddy-agent-${byte-buddy.version}.jar</argLine>
<systemPropertyVariables>
<mockito.mock-maker>subclass</mockito.mock-maker>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -2,7 +2,7 @@ package it.cnr.isti.workflow.manager.app;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapper;
@Component
public class ObjectMapperHolder {

View File

@ -8,8 +8,8 @@ import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode;
import it.cnr.isti.workflow.manager.blocks.Block;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
@ -131,7 +131,7 @@ public class BlockCatalogService {
}
Set<String> requiredFields = extractRequiredFields(schema.path("required"));
return iterable(schema.path("properties").fields()).stream()
return schema.path("properties").properties().stream()
.filter(entry -> !"type".equals(entry.getKey()))
.map(entry -> new AssistantPromptFieldDescriptor(
entry.getKey(),

View File

@ -15,8 +15,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ObjectNode;
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
import it.cnr.isti.workflow.manager.assistant.FlowAssistantPromptService.OperationMode;

View File

@ -3,6 +3,8 @@ package it.cnr.isti.workflow.manager.blocks;
import java.util.List;
import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
@ -20,13 +22,14 @@ import lombok.NonNull;
import lombok.Singular;
import lombok.ToString;
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED, onConstructor_ = @JsonCreator)
@Getter
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class Block<T extends BlockType> implements FlowNode {
final String id = UUID.randomUUID().toString();
String id = UUID.randomUUID().toString();
Position position;

View File

@ -5,7 +5,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
import tools.jackson.databind.annotation.JsonTypeIdResolver;
import it.cnr.isti.workflow.manager.blocks.types.BlockType;
import it.cnr.isti.workflow.manager.configurations.annotations.UiOrder;

View File

@ -52,7 +52,7 @@ public class ChatInteractionBlockConfiguration extends BlockConfiguration<ChatIn
public ChatInteractionBlockConfiguration(@NonNull String name, LLMDescriptor llmDescriptor,
String goalDescription,
List<ChatInteractionInput> inputs,
Boolean exposeHistory) {
@JsonProperty(value = "exposeHistory", required = false) Boolean exposeHistory) {
super(name);
this.llmDescriptor = llmDescriptor;
this.goalDescription = goalDescription;

View File

@ -1,9 +1,10 @@
package it.cnr.isti.workflow.manager.blocks.configurations;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.DatabindContext;
import tools.jackson.databind.jsontype.impl.TypeIdResolverBase;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.DatabindContext;
import tools.jackson.core.JacksonException;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ScanResult;
@ -36,21 +37,33 @@ public class DynamicBlockConfigurationTypeResolver extends TypeIdResolverBase {
}
@Override
public String idFromValue(Object value) {
public String idFromValue(DatabindContext context, Object value) throws JacksonException {
return classToId.get(value.getClass());
}
@Override
public String idFromValueAndType(Object value, Class<?> suggestedType) {
public String idFromValueAndType(DatabindContext context, Object value, Class<?> suggestedType) throws JacksonException {
return classToId.get(suggestedType);
}
@Override
public JavaType typeFromId(DatabindContext context, String id) {
public JavaType typeFromId(DatabindContext context, String id) throws JacksonException {
if (CHAT_INTERACTION_LEGACY_CONFIGURATION_ID.equals(id)) {
id = CHAT_INTERACTION_CONFIGURATION_ID;
}
Class<?> clazz = idToClass.get(id);
if (clazz == null && id != null) {
String normalized = id;
int suffixSeparator = normalized.indexOf("__");
if (suffixSeparator > 0) {
normalized = normalized.substring(0, suffixSeparator);
}
int lastDot = normalized.lastIndexOf('.');
if (lastDot >= 0 && lastDot < normalized.length() - 1) {
normalized = normalized.substring(lastDot + 1);
}
clazz = idToClass.get(normalized);
}
if (clazz == null) {
throw new IllegalArgumentException("Unknown BlockConfiguration type id: " + id);
}

View File

@ -28,7 +28,8 @@ public class HumanInteractiveBlockConfiguration extends BlockConfiguration<Human
}
@Builder
public HumanInteractiveBlockConfiguration(@NonNull String name, @NonNull String actionDescription) {
public HumanInteractiveBlockConfiguration(@NonNull String name,
@JsonProperty(value = "actionDescription", required = false) String actionDescription) {
super(name);
this.actionDescription = actionDescription;
}

View File

@ -10,10 +10,10 @@ import java.util.Set;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
@Component
public class JsonSchemaCatalogBundler {
@ -71,7 +71,7 @@ public class JsonSchemaCatalogBundler {
if (definitions == null) {
continue;
}
for (Entry<String, JsonNode> entry : iterable(definitions.fields())) {
for (Entry<String, JsonNode> entry : definitions.properties()) {
definitionsByName
.computeIfAbsent(entry.getKey(), ignored -> new DefinitionStats())
.register(entry.getValue());
@ -96,8 +96,8 @@ public class JsonSchemaCatalogBundler {
objectNode.put("$ref", rewritten);
}
}
for (JsonNode child : iterable(objectNode.elements())) {
rewriteSharedRefs(child, sharedNames);
for (Map.Entry<String, JsonNode> child : objectNode.properties()) {
rewriteSharedRefs(child.getValue(), sharedNames);
}
return;
}

View File

@ -17,15 +17,21 @@ import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;
import java.util.Deque;
import java.util.Arrays;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.kjetland.jackson.jsonSchema.JsonSchemaConfig;
import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
import com.github.victools.jsonschema.generator.Option;
import com.github.victools.jsonschema.generator.OptionPreset;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
import com.github.victools.jsonschema.module.jackson.JacksonModule;
import com.github.victools.jsonschema.module.jackson.JacksonOption;
import it.cnr.isti.workflow.manager.configurations.annotations.FieldRetriever;
import it.cnr.isti.workflow.manager.configurations.annotations.LongText;
@ -41,22 +47,32 @@ import it.cnr.isti.workflow.manager.configurations.annotations.UiOrder;
import it.cnr.isti.workflow.manager.configurations.annotations.UiOptionsFromNode;
import it.cnr.isti.workflow.manager.configurations.annotations.UiUniqueItemsBy;
import jakarta.validation.constraints.Size;
import com.fasterxml.jackson.annotation.JsonProperty;
import it.cnr.isti.workflow.manager.containers.configurations.ContainerConfiguration;
@Component
public class JsonSchemaProducer {
private final JsonSchemaGenerator schemaGenerator;
private final SchemaGenerator schemaGenerator;
public JsonSchemaProducer(ObjectMapper objectMapper) {
this.schemaGenerator = new JsonSchemaGenerator(objectMapper, JsonSchemaConfig.vanillaJsonSchemaDraft4());
public JsonSchemaProducer() {
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(
SchemaVersion.DRAFT_7, OptionPreset.PLAIN_JSON)
.with(Option.DEFINITIONS_FOR_ALL_OBJECTS)
.with(new JacksonModule(JacksonOption.IGNORE_TYPE_INFO_TRANSFORM));
this.schemaGenerator = new SchemaGenerator(configBuilder.build());
}
public JsonNode generateSchemaNode(Class<?> type) {
JsonNode schema = schemaGenerator.generateJsonSchema(type);
JsonNode schema = schemaGenerator.generateSchema(type);
if (!(schema instanceof ObjectNode root)) {
return schema;
}
ensureDiscriminatorProperties(root, type);
applyRequiredMetadata(root, collectRequiredPropertyNames(type));
applyEnumMetadata(root, collectEnumValues(type));
Map<Class<?>, Map<String, FieldRetriever>> retrieverMap = collectRetrieverMetadata(type);
Map<Class<?>, Map<String, DynamicSchema>> dynamicSchemaMap = collectDynamicSchemaMetadata(type);
Map<Class<?>, Map<String, LongText>> longTextMap = collectLongTextMetadata(type);
@ -102,12 +118,15 @@ public class JsonSchemaProducer {
metadataClasses.addAll(sizeMap.keySet());
metadataClasses.addAll(schemaAllowedValuesMap.keySet());
metadataClasses.addAll(configurableAsInputMap.keySet());
for (Entry<String, JsonNode> entry : iterable(definitions.fields())) {
for (Entry<String, JsonNode> entry : definitions.properties()) {
if (!(entry.getValue() instanceof ObjectNode classSchema)) {
continue;
}
Class<?> matchedClass = findMatchingClass(entry.getKey(), metadataClasses);
if (matchedClass != null) {
ensureDiscriminatorProperties(classSchema, matchedClass);
applyRequiredMetadata(classSchema, collectRequiredPropertyNames(matchedClass));
applyEnumMetadata(classSchema, collectEnumValues(matchedClass));
applyRetrieverMetadata(classSchema, matchedClass, getMergedMetadata(retrieverMap, matchedClass));
applyDynamicSchemaMetadata(classSchema, getMergedMetadata(dynamicSchemaMap, matchedClass));
applyLongTextMetadata(classSchema, getMergedMetadata(longTextMap, matchedClass));
@ -129,6 +148,150 @@ public class JsonSchemaProducer {
return root;
}
private void ensureDiscriminatorProperties(ObjectNode classSchema, Class<?> ownerClass) {
JsonNode propsNode = classSchema.get("properties");
if (!(propsNode instanceof ObjectNode properties)) {
return;
}
if (BlockConfiguration.class.isAssignableFrom(ownerClass)) {
ensureStringProperty(properties, "type");
}
if (ContainerConfiguration.class.isAssignableFrom(ownerClass)) {
ensureStringProperty(properties, "containerType");
ensureStringProperty(properties, "type");
}
}
private void ensureStringProperty(ObjectNode properties, String propertyName) {
JsonNode existing = properties.get(propertyName);
if (existing instanceof ObjectNode) {
return;
}
ObjectNode property = properties.putObject(propertyName);
property.put("type", "string");
}
private Set<String> collectRequiredPropertyNames(Class<?> rootClass) {
Set<String> required = new LinkedHashSet<>();
Deque<Class<?>> hierarchy = new ArrayDeque<>();
Class<?> current = rootClass;
while (current != null && current != Object.class) {
hierarchy.addFirst(current);
current = current.getSuperclass();
}
for (Class<?> clazz : hierarchy) {
for (Field field : clazz.getDeclaredFields()) {
JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class);
if (jsonProperty != null && jsonProperty.required()) {
required.add(field.getName());
}
}
if (clazz.isRecord()) {
for (RecordComponent component : clazz.getRecordComponents()) {
JsonProperty jsonProperty = component.getAnnotation(JsonProperty.class);
if (jsonProperty != null && jsonProperty.required()) {
required.add(component.getName());
}
}
}
}
return required;
}
private void applyRequiredMetadata(ObjectNode classSchema, Set<String> requiredNames) {
if (requiredNames == null || requiredNames.isEmpty()) {
return;
}
JsonNode propsNode = classSchema.get("properties");
if (!(propsNode instanceof ObjectNode properties)) {
return;
}
ArrayNode requiredArray;
JsonNode existingRequired = classSchema.get("required");
if (existingRequired instanceof ArrayNode existingArray) {
requiredArray = existingArray;
} else {
requiredArray = classSchema.putArray("required");
}
Set<String> currentRequired = new LinkedHashSet<>();
for (JsonNode node : requiredArray) {
if (node.isTextual()) {
currentRequired.add(node.asText());
}
}
for (String requiredName : requiredNames) {
if (properties.has(requiredName) && currentRequired.add(requiredName)) {
requiredArray.add(requiredName);
}
}
}
private Map<String, List<String>> collectEnumValues(Class<?> rootClass) {
Map<String, List<String>> enumValues = new LinkedHashMap<>();
Deque<Class<?>> hierarchy = new ArrayDeque<>();
Class<?> current = rootClass;
while (current != null && current != Object.class) {
hierarchy.addFirst(current);
current = current.getSuperclass();
}
for (Class<?> clazz : hierarchy) {
for (Field field : clazz.getDeclaredFields()) {
if (field.getType().isEnum()) {
enumValues.put(field.getName(), enumConstantNames(field.getType()));
}
}
if (clazz.isRecord()) {
for (RecordComponent component : clazz.getRecordComponents()) {
if (component.getType().isEnum()) {
enumValues.put(component.getName(), enumConstantNames(component.getType()));
}
}
}
}
return enumValues;
}
private List<String> enumConstantNames(Class<?> enumType) {
return Arrays.stream(enumType.getEnumConstants())
.map(value -> ((Enum<?>) value).name())
.toList();
}
private void applyEnumMetadata(ObjectNode classSchema, Map<String, List<String>> enumValues) {
if (enumValues == null || enumValues.isEmpty()) {
return;
}
JsonNode propsNode = classSchema.get("properties");
if (!(propsNode instanceof ObjectNode properties)) {
return;
}
for (Entry<String, List<String>> entry : enumValues.entrySet()) {
JsonNode propNode = properties.get(entry.getKey());
if (!(propNode instanceof ObjectNode propertySchema)) {
continue;
}
JsonNode existingEnum = propertySchema.get("enum");
if (existingEnum instanceof ArrayNode existingArray && !existingArray.isEmpty()) {
continue;
}
ArrayNode enumArray = propertySchema.putArray("enum");
for (String value : entry.getValue()) {
enumArray.add(value);
}
}
}
private <A> Map<String, A> getMergedMetadata(Map<Class<?>, Map<String, A>> metadataMap, Class<?> type) {
if (type == null || metadataMap == null || metadataMap.isEmpty()) {
return Map.of();
@ -918,7 +1081,7 @@ public class JsonSchemaProducer {
}
List<String> propertyNames = new ArrayList<>();
for (String propertyName : iterable(properties.fieldNames())) {
for (String propertyName : properties.propertyNames()) {
propertyNames.add(propertyName);
}
if (propertyNames.isEmpty()) {
@ -1115,7 +1278,9 @@ public class JsonSchemaProducer {
for (Class<?> candidate : candidates) {
if (candidate.getSimpleName().equals(schemaKey)
|| candidate.getName().equals(schemaKey)
|| schemaKey.endsWith("." + candidate.getSimpleName())) {
|| schemaKey.endsWith("." + candidate.getSimpleName())
|| schemaKey.startsWith(candidate.getSimpleName() + "__")
|| schemaKey.startsWith(candidate.getName() + "__")) {
return candidate;
}
}

View File

@ -46,7 +46,9 @@ public class LLMBlockConfiguration extends BlockConfiguration<LLMBlockType> {
List<SkillBinding> skills = List.of();
@Builder
public LLMBlockConfiguration(@NonNull String name, @NonNull LLMDescriptor llmDescriptor, String prompt,
public LLMBlockConfiguration(@NonNull String name,
@JsonProperty(value = "llmDescriptor", required = false) LLMDescriptor llmDescriptor,
String prompt,
List<SkillBinding> skills) {
super(name);
this.llmDescriptor = llmDescriptor;

View File

@ -5,7 +5,7 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.databind.JsonNode;
import it.cnr.isti.workflow.manager.configurations.annotations.FieldRetriever;
import it.cnr.isti.workflow.manager.configurations.annotations.LongText;

View File

@ -5,7 +5,7 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.databind.JsonNode;
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentChatBlockType;
import it.cnr.isti.workflow.manager.configurations.annotations.ConfigurableAsInput;

View File

@ -6,6 +6,7 @@ import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonAlias;
import it.cnr.isti.workflow.manager.blocks.Position;
import it.cnr.isti.workflow.manager.containers.configurations.ContainerConfiguration;
@ -27,7 +28,7 @@ import lombok.ToString;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Container<T extends ContainerType> implements FlowNode {
final String id = UUID.randomUUID().toString();
String id = UUID.randomUUID().toString();
Position position;
@ -57,14 +58,16 @@ public class Container<T extends ContainerType> implements FlowNode {
@Builder
public Container(@NonNull ContainerConfiguration<T> specificConfiguration, @Singular List<IODescriptor> inputs,
@Singular List<IODescriptor> outputs, @NonNull T type, Position position) {
@Singular List<IODescriptor> outputs, T type,
@JsonProperty("typeName") @JsonAlias("type") String resolvedTypeName,
Position position) {
this.specificConfiguration = specificConfiguration;
this.name = specificConfiguration.getName();
this.inputs = inputs;
this.outputs = outputs;
this.typeName = type.getName();
this.typeName = type != null ? type.getName() : resolvedTypeName;
this.position = position;
this.type = type;
this.type = type != null ? type : (T) ContainerTypes.get(this.typeName);
}
@Override

View File

@ -3,7 +3,7 @@ package it.cnr.isti.workflow.manager.containers.configurations;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
import tools.jackson.databind.annotation.JsonTypeIdResolver;
import it.cnr.isti.workflow.manager.configurations.annotations.FieldRetriever;
import it.cnr.isti.workflow.manager.configurations.annotations.Structural;

View File

@ -1,9 +1,10 @@
package it.cnr.isti.workflow.manager.containers.configurations;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DatabindContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase;
import tools.jackson.databind.DatabindContext;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.jsontype.impl.TypeIdResolverBase;
import tools.jackson.core.JacksonException;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ScanResult;
@ -32,18 +33,30 @@ public class DynamicContainerConfigurationTypeResolver extends TypeIdResolverBas
}
@Override
public String idFromValue(Object value) {
public String idFromValue(DatabindContext context, Object value) throws JacksonException {
return classToId.get(value.getClass());
}
@Override
public String idFromValueAndType(Object value, Class<?> suggestedType) {
public String idFromValueAndType(DatabindContext context, Object value, Class<?> suggestedType) throws JacksonException {
return classToId.get(suggestedType);
}
@Override
public JavaType typeFromId(DatabindContext context, String id) {
public JavaType typeFromId(DatabindContext context, String id) throws JacksonException {
Class<?> clazz = idToClass.get(id);
if (clazz == null && id != null) {
String normalized = id;
int suffixSeparator = normalized.indexOf("__");
if (suffixSeparator > 0) {
normalized = normalized.substring(0, suffixSeparator);
}
int lastDot = normalized.lastIndexOf('.');
if (lastDot >= 0 && lastDot < normalized.length() - 1) {
normalized = normalized.substring(lastDot + 1);
}
clazz = idToClass.get(normalized);
}
if (clazz == null) {
throw new IllegalArgumentException("Unknown ContainerConfiguration type id: " + id);
}

View File

@ -26,8 +26,8 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.springframework.web.server.ResponseStatusException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.core.JsonProcessingException;
import tools.jackson.databind.JsonNode;
import tools.jackson.core.JacksonException;
@RestController
@RequestMapping("/blocks")
@ -243,7 +243,7 @@ public class BlocksController {
String blockAsJson = null;
try {
blockAsJson = ObjectMapperHolder.mapper.writeValueAsString(block);
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
logger.error("Failed to serialize block to JSON", e);
}
logger.debug("Block created: {}", blockAsJson);

View File

@ -26,7 +26,7 @@ import it.cnr.isti.workflow.manager.blocks.configurations.JsonSchemaCatalogBundl
import it.cnr.isti.workflow.manager.flows.model.FlowData;
import it.cnr.isti.workflow.manager.flows.validation.ValidationError;
import it.cnr.isti.workflow.manager.ios.IODescriptor;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.databind.JsonNode;
@RestController
@RequestMapping("/containers")

View File

@ -5,10 +5,10 @@ import java.util.List;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.http.HttpStatus;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

View File

@ -14,8 +14,8 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import it.cnr.isti.workflow.manager.blocks.Block;
import it.cnr.isti.workflow.manager.blocks.configurations.ConditionalBlockConfiguration;

View File

@ -15,8 +15,8 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import it.cnr.isti.workflow.manager.blocks.Block;
import it.cnr.isti.workflow.manager.blocks.configurations.SwitchBlockConfiguration;

View File

@ -15,8 +15,8 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import it.cnr.isti.workflow.manager.containers.Container;
import it.cnr.isti.workflow.manager.containers.configurations.LoopContainerConfiguration;

View File

@ -1,7 +1,9 @@
package it.cnr.isti.workflow.manager.executions.persistence;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
import jakarta.persistence.AttributeConverter;
@ -10,7 +12,9 @@ import jakarta.persistence.Converter;
@Converter(autoApply = false)
public class ExecutionSnapshotConverter implements AttributeConverter<ExecutionSnapshot, String> {
private static final ObjectMapper FALLBACK_MAPPER = new ObjectMapper();
private static final ObjectMapper FALLBACK_MAPPER = JsonMapper.builder()
.changeDefaultVisibility(vc -> vc.withFieldVisibility(JsonAutoDetect.Visibility.ANY))
.build();
@Override
public String convertToDatabaseColumn(ExecutionSnapshot snapshot) {
@ -19,7 +23,7 @@ public class ExecutionSnapshotConverter implements AttributeConverter<ExecutionS
}
try {
return mapper().writeValueAsString(snapshot);
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
throw new IllegalArgumentException("Errore nella serializzazione di ExecutionSnapshot in JSON", e);
}
}

View File

@ -1,7 +1,9 @@
package it.cnr.isti.workflow.manager.flows;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import it.cnr.isti.workflow.manager.flows.model.FlowData;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
@ -9,7 +11,9 @@ import jakarta.persistence.Converter;
@Converter(autoApply = false)
public class FlowConverter implements AttributeConverter<FlowData, String> {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final ObjectMapper objectMapper = JsonMapper.builder()
.changeDefaultVisibility(vc -> vc.withFieldVisibility(JsonAutoDetect.Visibility.ANY))
.build();
@Override
public String convertToDatabaseColumn(FlowData flowData) {
@ -18,7 +22,7 @@ public class FlowConverter implements AttributeConverter<FlowData, String> {
}
try {
return objectMapper.writeValueAsString(flowData);
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
throw new IllegalArgumentException("Errore nella serializzazione di FlowData in JSON", e);
}
}

View File

@ -2,7 +2,7 @@ package it.cnr.isti.workflow.manager.flows.validation;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import tools.jackson.core.JacksonException;
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
@ -14,7 +14,7 @@ public final class ValidationErrorCodec {
public static String encode(List<ValidationError> errors) {
try {
return ObjectMapperHolder.mapper.writeValueAsString(errors);
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
throw new IllegalStateException("Unable to encode validation errors", e);
}
}

View File

@ -11,7 +11,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapper;
import it.cnr.isti.workflow.manager.llms.ChatMessage;
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;

View File

@ -16,7 +16,7 @@ import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono;

View File

@ -12,8 +12,8 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
@Component
public class MCPServersProvider {

View File

@ -17,7 +17,7 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapper;
@Component
public class SkillsCatalogService {

View File

@ -1,9 +1,5 @@
spring.application.name=workflow-manager
# Use Jackson 2 as the preferred JSON mapper for HTTP message conversion
# (project uses com.fasterxml.jackson annotations, Jackson 3 is incompatible)
spring.http.converters.preferred-json-mapper=jackson2
#postgresql details
spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/mydb}
spring.datasource.username=${DB_USER:lucio}
@ -53,3 +49,7 @@ app.turnstile.secret=${TURNSTILE_SECRET:}
app.turnstile.verify-url=${TURNSTILE_VERIFY_URL:https://challenges.cloudflare.com/turnstile/v0/siteverify}
logging.level.it.cnr.isti.workflow.manager=DEBUG
logging.level.root=ERROR
# Jackson 3 changed default field visibility from ANY (Jackson 2) to PUBLIC_ONLY.
# Restore to ANY to match Jackson 2 behavior for package-private fields in domain model.
spring.jackson.visibility.field=any

View File

@ -16,7 +16,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.test.context.TestPropertySource;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.databind.JsonNode;
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
import it.cnr.isti.workflow.manager.blocks.Block;
@ -549,9 +549,8 @@ public class BlocksControllerTest {
JsonNode inputs = schema.path("properties").path("inputs");
assertEquals("name", inputs.path("x-ui-unique-by").asText());
JsonNode definitions = schema.has("definitions") ? schema.path("definitions") : schema.path("$defs");
JsonNode inputDefinition = definitions.fields().next().getValue();
for (java.util.Iterator<java.util.Map.Entry<String, JsonNode>> it = definitions.fields(); it.hasNext();) {
java.util.Map.Entry<String, JsonNode> entry = it.next();
JsonNode inputDefinition = definitions.properties().iterator().next().getValue();
for (java.util.Map.Entry<String, JsonNode> entry : definitions.properties()) {
if (entry.getKey().contains("ChatInteractionInput")) {
inputDefinition = entry.getValue();
break;
@ -922,9 +921,7 @@ public class BlocksControllerTest {
}
private List<String> propertyNames(JsonNode objectNode) {
List<String> names = new java.util.ArrayList<>();
objectNode.fieldNames().forEachRemaining(names::add);
return names;
return objectNode.properties().stream().map(java.util.Map.Entry::getKey).collect(java.util.stream.Collectors.toList());
}
private List<String> arrayValues(JsonNode arrayNode) {

View File

@ -13,7 +13,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.databind.JsonNode;
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
import it.cnr.isti.workflow.manager.blocks.Block;
@ -527,9 +527,7 @@ public class ContainersControllerTest {
}
private List<String> propertyNames(JsonNode objectNode) {
List<String> names = new java.util.ArrayList<>();
objectNode.fieldNames().forEachRemaining(names::add);
return names;
return objectNode.properties().stream().map(java.util.Map.Entry::getKey).collect(java.util.stream.Collectors.toList());
}
private List<String> arrayValues(JsonNode arrayNode) {

View File

@ -15,8 +15,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.web.server.ResponseStatusException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.JsonNode;
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
@ -179,7 +179,7 @@ public class ExecutionControllerTest {
try {
logger.info("Execution object: {}", ObjectMapperHolder.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(executionObject));
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
throw new RuntimeException(e);
}
}
@ -408,7 +408,7 @@ public class ExecutionControllerTest {
}
@Test
public void executionPayloadDoesNotSerializeEvents() throws JsonProcessingException {
public void executionPayloadDoesNotSerializeEvents() throws JacksonException {
LLMDescriptor llmDescriptor = LLMDescriptor.builder()
.provider("testProvider")
.model("testModel")

View File

@ -19,9 +19,9 @@ import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.server.ResponseStatusException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
import it.cnr.isti.workflow.manager.auth.config.JwtUtil;
@ -125,7 +125,7 @@ public class FlowControllerTest {
try {
System.out.println(ObjectMapperHolder.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(retrieved));
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
throw new RuntimeException(e);
}
@ -688,7 +688,7 @@ public class FlowControllerTest {
}
@Test
public void createFlowRejectsBlockTamperedOutsideFactory() throws JsonProcessingException {
public void createFlowRejectsBlockTamperedOutsideFactory() throws JacksonException {
LLMDescriptor llmDescriptor = LLMDescriptor.builder()
.provider("testProvider")
.model("testModel")

View File

@ -75,7 +75,7 @@ public class RetrieverControllerTest {
@Test
public void customMcpServerSchemaExposesFreeConfiguration() {
com.fasterxml.jackson.databind.JsonNode schema = mcpServersController.getConfigurationSchema("CUSTOM", null);
tools.jackson.databind.JsonNode schema = mcpServersController.getConfigurationSchema("CUSTOM", null);
assertEquals("object", schema.path("type").asText());
assertTrue(schema.path("additionalProperties").asBoolean());

View File

@ -21,8 +21,8 @@ import org.springframework.test.context.TestPropertySource;
import org.springframework.util.ResourceUtils;
import org.springframework.web.server.ResponseStatusException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;

View File

@ -16,7 +16,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.test.context.TestPropertySource;
import org.testcontainers.containers.GenericContainer;
import com.fasterxml.jackson.core.JsonProcessingException;
import tools.jackson.core.JacksonException;
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
import it.cnr.isti.workflow.manager.configuration.TestContainersConfiguration;
@ -153,7 +153,7 @@ public class ExecutionWithContainer {
try {
logger.info("Execution object: {}", ObjectMapperHolder.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(execObject));
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
throw new RuntimeException(e);
}
@ -168,7 +168,7 @@ public class ExecutionWithContainer {
assertEquals(ExecutionStatus.CREATED, execObject.getContext().getStatus());
try {
logger.info("Execution object in CREATED State: {}", ObjectMapperHolder.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(execObject));
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
throw new RuntimeException(e);
}
for (Step<?> s : execObject.getContext().getSteps().values()) {
@ -181,7 +181,7 @@ public class ExecutionWithContainer {
try {
logger.info("Execution object in READY State: {}", ObjectMapperHolder.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(execObject));
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
throw new RuntimeException(e);
}
@ -201,7 +201,7 @@ public class ExecutionWithContainer {
try {
logger.info("Execution object in WAITING State: {}", ObjectMapperHolder.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(execObject));
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
throw new RuntimeException(e);
}

View File

@ -5,7 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapper;
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
import it.cnr.isti.workflow.manager.flows.model.Flow;
import it.cnr.isti.workflow.manager.flows.model.FlowCreateRequest;

View File

@ -14,7 +14,7 @@ import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.web.reactive.function.client.WebClient;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapper;
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentBlockConfiguration;

View File

@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.io.DefaultResourceLoader;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapper;
public class MCPServersProviderTest {

View File

@ -6,7 +6,7 @@ import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapper;
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
import reactor.core.publisher.Mono;