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.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<String, Consumer<String>> setterMap = new HashMap<>();
Map<String, Consumer<Object>> 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<? 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
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -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<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) //
@ -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<Object> 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));

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));
}
}