From d4657989dd81287e0572a24fc2331e2829f944f7 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 11 Jul 2022 09:08:09 -0500 Subject: [PATCH] Working towards fixing serialization of qstep metadata --- .../serialization/DeserializerUtils.java | 105 +++++++++++++----- .../QStepMetaDataDeserializer.java | 56 ++++++++++ 2 files changed, 135 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/kingsrook/qqq/backend/core/model/metadata/serialization/QStepMetaDataDeserializer.java diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/serialization/DeserializerUtils.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/serialization/DeserializerUtils.java index 98b7621f..17462cfe 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/serialization/DeserializerUtils.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/serialization/DeserializerUtils.java @@ -26,18 +26,25 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Consumer; import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.modules.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.interfaces.QBackendModuleInterface; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -53,32 +60,42 @@ public class DeserializerUtils + /******************************************************************************* + ** Read a string value, identified by key, from a jackson treeNode. + *******************************************************************************/ + public static String readTextValue(TreeNode treeNode, String key) throws IOException + { + TreeNode valueNode = treeNode.get(key); + if(valueNode == null || valueNode instanceof NullNode) + { + throw new IOException("Missing node named [" + key + "]"); + } + + if(!(valueNode instanceof TextNode textNode)) + { + throw new IOException(key + "is not a string value (is: " + valueNode.getClass().getSimpleName() + ")"); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // get the value of the backendType json node, and use it to look up the qBackendModule object // + ///////////////////////////////////////////////////////////////////////////////////////////////// + return (textNode.asText()); + } + + + /******************************************************************************* ** For a given (jackson, JSON) treeNode, look at its backendType property, ** and return an instance of the corresponding QBackendModule. *******************************************************************************/ public static QBackendModuleInterface getBackendModule(TreeNode treeNode) throws IOException { - ///////////////////////////////////////////////////////////////////////////////// - // validate the backendType property is present, as text, in the json treeNode // - ///////////////////////////////////////////////////////////////////////////////// - TreeNode backendTypeTreeNode = treeNode.get("backendType"); - if(backendTypeTreeNode == null || backendTypeTreeNode instanceof NullNode) - { - throw new IOException("Missing backendType in backendMetaData"); - } - - if(!(backendTypeTreeNode instanceof TextNode textNode)) - { - throw new IOException("backendType is not a string value (is: " + backendTypeTreeNode.getClass().getSimpleName() + ")"); - } - try { ///////////////////////////////////////////////////////////////////////////////////////////////// // get the value of the backendType json node, and use it to look up the qBackendModule object // ///////////////////////////////////////////////////////////////////////////////////////////////// - String backendType = textNode.asText(); + String backendType = readTextValue(treeNode, "backendType"); return new QBackendModuleDispatcher().getQBackendModule(backendType); } catch(QModuleDispatchException e) @@ -108,7 +125,7 @@ public class DeserializerUtils // and set it in the output object, doing type conversion as needed. // // do this by iterating over methods on the output class that look like setters. // ///////////////////////////////////////////////////////////////////////////////////////////////// - Map> setterMap = new HashMap<>(); + Map> setterMap = new HashMap<>(); for(Method method : outputClass.getMethods()) { ///////////////////////////////////////////////////////////// @@ -125,33 +142,41 @@ public class DeserializerUtils /////////////////////////////////////////////////////////////////////////////////// // put the entry in the map - where the value here is a consumer lambda function // /////////////////////////////////////////////////////////////////////////////////// - setterMap.put(fieldName, (String value) -> + setterMap.put(fieldName, (Object value) -> { try { + if(value == null) + { + Object[] args = new Object[] { null }; + method.invoke(output, args); + return; + } + ////////////////////////////////////////////////////////////////////////////////////////////////// // based on the parameter type, handle it differently - either type-converting (e.g., parseInt) // // or gracefully ignoring, or failing. // ////////////////////////////////////////////////////////////////////////////////////////////////// + String valueString = String.valueOf(value); if(parameterType.equals(String.class)) { - method.invoke(output, value); + method.invoke(output, String.valueOf(value)); } else if(parameterType.equals(Integer.class)) { - method.invoke(output, StringUtils.hasContent(value) ? Integer.parseInt(value) : null); + method.invoke(output, ValueUtils.getValueAsInteger(value)); } else if(parameterType.equals(Long.class)) { - method.invoke(output, StringUtils.hasContent(value) ? Long.parseLong(value) : null); + method.invoke(output, StringUtils.hasContent(valueString) ? Long.parseLong(valueString) : null); } else if(parameterType.equals(BigDecimal.class)) { - method.invoke(output, StringUtils.hasContent(value) ? new BigDecimal(value) : null); + method.invoke(output, StringUtils.hasContent(valueString) ? new BigDecimal(valueString) : null); } else if(parameterType.equals(Boolean.class)) { - method.invoke(output, StringUtils.hasContent(value) ? Boolean.parseBoolean(value) : null); + method.invoke(output, StringUtils.hasContent(valueString) ? Boolean.parseBoolean(valueString) : null); } else if(parameterType.isEnum()) { @@ -160,7 +185,7 @@ public class DeserializerUtils // call that method, passing it the string value from the json (the null there is because it's a static method) // // then pass the num value into the output object, via our method.invoke. // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(StringUtils.hasContent(value)) + if(StringUtils.hasContent(valueString)) { Method valueOfMethod = parameterType.getMethod("valueOf", String.class); Object enumValue = valueOfMethod.invoke(null, value); @@ -178,6 +203,21 @@ public class DeserializerUtils // we hit this when trying to de-serialize a QBackendMetaData, and we found its setBackendType(Class) method // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// } + else if(parameterType.isAssignableFrom(List.class)) + { + if(value instanceof List) + { + method.invoke(output, value); + } + } + else if(parameterType.isAssignableFrom(Map.class)) + { + // TypeVariable> keyType = parameterType.getTypeParameters()[0]; + // TypeVariable> valueType = parameterType.getTypeParameters()[1]; + Map map = new LinkedHashMap<>(); + // todo - recursively process + method.invoke(output, map); + } else { ///////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -185,10 +225,10 @@ public class DeserializerUtils // otherwise, either find some jackson annotation that makes sense, and apply it to the setter method, // // or if no jackson annotation is right, then come up with annotation of our own. // ///////////////////////////////////////////////////////////////////////////////////////////////////////// - throw (new RuntimeException("Field " + fieldName + " is of an unhandled type " + parameterType.getName() + " when deserializing " + outputClass.getName())); + method.invoke(output, reflectivelyDeserialize((Class) parameterType, (TreeNode) value)); } } - catch(IllegalAccessException | InvocationTargetException | NoSuchMethodException e) + catch(IllegalAccessException | InvocationTargetException | NoSuchMethodException | IOException e) { throw new RuntimeException(e); } @@ -216,7 +256,7 @@ public class DeserializerUtils ** Note, the consumers in the map all work on strings, so you may need to do ** Integer.parseInt, for example, in a lambda in the map. *******************************************************************************/ - private static void deserializeBean(TreeNode treeNode, Map> setterMap) throws IOException + private static void deserializeBean(TreeNode treeNode, Map> setterMap) throws IOException { /////////////////////////////////////////////////////// // iterate over fields in the json object (treeNode) // @@ -244,6 +284,19 @@ public class DeserializerUtils { setterMap.get(fieldName).accept(textNode.asText()); } + else if(fieldNode instanceof ObjectNode) + { + setterMap.get(fieldName).accept(fieldNode); + } + else if(fieldNode instanceof ArrayNode arrayNode) + { + List list = new ArrayList<>(); + for(JsonNode jsonNode : arrayNode) + { + // todo - actually build the objects... + } + setterMap.get(fieldName).accept(list); + } else { throw (new IOException("Unexpected node type (" + fieldNode.getClass() + ") for field: " + fieldName)); diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/serialization/QStepMetaDataDeserializer.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/serialization/QStepMetaDataDeserializer.java new file mode 100644 index 00000000..4b7608cf --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/serialization/QStepMetaDataDeserializer.java @@ -0,0 +1,56 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.serialization; + + +import java.io.IOException; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; + + +/******************************************************************************* + ** Jackson custom deserialization class, to return an appropriate sub-type of + ** QTableBackendDetails, based on the backendType of the containing table. + *******************************************************************************/ +public class QStepMetaDataDeserializer extends JsonDeserializer +{ + @Override + @SuppressWarnings("checkstyle:Indentation") + public QStepMetaData deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException + { + TreeNode treeNode = jsonParser.readValueAsTree(); + String stepType = DeserializerUtils.readTextValue(treeNode, "stepType"); + Class targetClass = switch(stepType) + { + case "backend" -> QBackendStepMetaData.class; + case "frontend" -> QBackendStepMetaData.class; + default -> throw new IllegalArgumentException("Unsupported StepType " + stepType + " for deserialization"); + }; + return (DeserializerUtils.reflectivelyDeserialize(targetClass, treeNode)); + } + +}