mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Working towards fixing serialization of qstep metadata
This commit is contained in:
@ -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));
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user