Working towards fixing serialization of qstep metadata

This commit is contained in:
2022-07-11 09:08:09 -05:00
parent b9ef76da13
commit d4657989dd
2 changed files with 135 additions and 26 deletions

View File

@ -26,18 +26,25 @@ import java.io.IOException;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import com.fasterxml.jackson.core.TreeNode; 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.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.databind.node.TextNode;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.modules.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.interfaces.QBackendModuleInterface; import com.kingsrook.qqq.backend.core.modules.interfaces.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.StringUtils; 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.LogManager;
import org.apache.logging.log4j.Logger; 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, ** For a given (jackson, JSON) treeNode, look at its backendType property,
** and return an instance of the corresponding QBackendModule. ** and return an instance of the corresponding QBackendModule.
*******************************************************************************/ *******************************************************************************/
public static QBackendModuleInterface getBackendModule(TreeNode treeNode) throws IOException 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 try
{ {
///////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////
// get the value of the backendType json node, and use it to look up the qBackendModule object // // 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); return new QBackendModuleDispatcher().getQBackendModule(backendType);
} }
catch(QModuleDispatchException e) catch(QModuleDispatchException e)
@ -108,7 +125,7 @@ public class DeserializerUtils
// and set it in the output object, doing type conversion as needed. // // 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. // // do this by iterating over methods on the output class that look like setters. //
///////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////
Map<String, Consumer<String>> setterMap = new HashMap<>(); Map<String, Consumer<Object>> setterMap = new HashMap<>();
for(Method method : outputClass.getMethods()) 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 // // 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 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) // // based on the parameter type, handle it differently - either type-converting (e.g., parseInt) //
// or gracefully ignoring, or failing. // // or gracefully ignoring, or failing. //
////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////
String valueString = String.valueOf(value);
if(parameterType.equals(String.class)) if(parameterType.equals(String.class))
{ {
method.invoke(output, value); method.invoke(output, String.valueOf(value));
} }
else if(parameterType.equals(Integer.class)) 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)) 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)) 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)) 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()) 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) // // 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. // // 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); Method valueOfMethod = parameterType.getMethod("valueOf", String.class);
Object enumValue = valueOfMethod.invoke(null, value); 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 // // 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<? extends Class<?>> keyType = parameterType.getTypeParameters()[0];
// TypeVariable<? extends Class<?>> valueType = parameterType.getTypeParameters()[1];
Map<?, ?> map = new LinkedHashMap<>();
// todo - recursively process
method.invoke(output, map);
}
else else
{ {
///////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -185,10 +225,10 @@ public class DeserializerUtils
// otherwise, either find some jackson annotation that makes sense, and apply it to the setter method, // // 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. // // 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); 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 ** 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. ** Integer.parseInt, for example, in a lambda in the map.
*******************************************************************************/ *******************************************************************************/
private static void deserializeBean(TreeNode treeNode, Map<String, Consumer<String>> setterMap) throws IOException private static void deserializeBean(TreeNode treeNode, Map<String, Consumer<Object>> setterMap) throws IOException
{ {
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
// iterate over fields in the json object (treeNode) // // iterate over fields in the json object (treeNode) //
@ -244,6 +284,19 @@ public class DeserializerUtils
{ {
setterMap.get(fieldName).accept(textNode.asText()); setterMap.get(fieldName).accept(textNode.asText());
} }
else if(fieldNode instanceof ObjectNode)
{
setterMap.get(fieldName).accept(fieldNode);
}
else if(fieldNode instanceof ArrayNode arrayNode)
{
List<Object> list = new ArrayList<>();
for(JsonNode jsonNode : arrayNode)
{
// todo - actually build the objects...
}
setterMap.get(fieldName).accept(list);
}
else else
{ {
throw (new IOException("Unexpected node type (" + fieldNode.getClass() + ") for field: " + fieldName)); throw (new IOException("Unexpected node type (" + fieldNode.getClass() + ") for field: " + fieldName));

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QStepMetaData>
{
@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<? extends QStepMetaData> 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));
}
}