mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-17 20:50:44 +00:00
Avoid exceptions from jackson serialization of processValues that contain a map with a null key
This commit is contained in:
@ -352,6 +352,8 @@ public class QJavalinProcessHandler
|
|||||||
Map<String, Object> resultForCaller = new HashMap<>();
|
Map<String, Object> resultForCaller = new HashMap<>();
|
||||||
Exception returningException = null;
|
Exception returningException = null;
|
||||||
|
|
||||||
|
String processName = context.pathParam("processName");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if(processUUID == null)
|
if(processUUID == null)
|
||||||
@ -360,7 +362,6 @@ public class QJavalinProcessHandler
|
|||||||
}
|
}
|
||||||
resultForCaller.put("processUUID", processUUID);
|
resultForCaller.put("processUUID", processUUID);
|
||||||
|
|
||||||
String processName = context.pathParam("processName");
|
|
||||||
LOG.info(startAfterStep == null ? "Initiating process [" + processName + "] [" + processUUID + "]"
|
LOG.info(startAfterStep == null ? "Initiating process [" + processName + "] [" + processUUID + "]"
|
||||||
: "Resuming process [" + processName + "] [" + processUUID + "] after step [" + startAfterStep + "]");
|
: "Resuming process [" + processName + "] [" + processUUID + "] after step [" + startAfterStep + "]");
|
||||||
|
|
||||||
@ -441,10 +442,30 @@ public class QJavalinProcessHandler
|
|||||||
// negative side-effects - but be aware. //
|
// negative side-effects - but be aware. //
|
||||||
// One could imagine that we'd need this to be configurable in the future? //
|
// One could imagine that we'd need this to be configurable in the future? //
|
||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
context.result(JsonUtils.toJson(resultForCaller, mapper ->
|
try
|
||||||
{
|
{
|
||||||
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
|
String json = JsonUtils.toJson(resultForCaller, mapper ->
|
||||||
}));
|
{
|
||||||
|
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// use this custom serializer to convert null map-keys to empty-strings (rather than having an exception!) //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
mapper.getSerializerProvider().setNullKeySerializer(JsonUtils.nullKeyToEmptyStringSerializer);
|
||||||
|
});
|
||||||
|
context.result(json);
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// related to the change above - we've seen at least one new error that can come from the //
|
||||||
|
// Include.ALWAYS change (a null key in a map -> jackson exception). So, try-catch around //
|
||||||
|
// the above serialization, and if it does throw, log, but continue trying a default serialization //
|
||||||
|
// as that is probably preferable to an exception for the caller... //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
LOG.warn("Error deserializing process results with serializationInclusion:ALWAYS - will retry with default settings", e, logPair("processName", processName), logPair("processUUID", processUUID));
|
||||||
|
context.result(JsonUtils.toJson(resultForCaller));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ import java.io.Serializable;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepOrStatusOutputInterface;
|
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepOrStatusOutputInterface;
|
||||||
import com.kingsrook.qqq.middleware.javalin.schemabuilder.SchemaBuilder;
|
import com.kingsrook.qqq.middleware.javalin.schemabuilder.SchemaBuilder;
|
||||||
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
|
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
|
||||||
@ -117,6 +118,7 @@ public class ProcessInitOrStepOrStatusResponseV1 implements ProcessInitOrStepOrS
|
|||||||
** Getter for values
|
** Getter for values
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@JsonIgnore // we are doing custom serialization of the values map, so mark as ignore.
|
||||||
public Map<String, Serializable> getValues()
|
public Map<String, Serializable> getValues()
|
||||||
{
|
{
|
||||||
return values;
|
return values;
|
||||||
|
@ -183,6 +183,14 @@ public class ProcessSpecUtilsV1
|
|||||||
String name = valueEntry.getKey();
|
String name = valueEntry.getKey();
|
||||||
Serializable value = valueEntry.getValue();
|
Serializable value = valueEntry.getValue();
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// follow the strategy that we use for JsonUtils.nullKeyToEmptyStringSerializer in this rare case... //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(name == null)
|
||||||
|
{
|
||||||
|
name = "";
|
||||||
|
}
|
||||||
|
|
||||||
Serializable valueToMakeIntoJson = value;
|
Serializable valueToMakeIntoJson = value;
|
||||||
if(value instanceof String s)
|
if(value instanceof String s)
|
||||||
{
|
{
|
||||||
@ -213,11 +221,31 @@ public class ProcessSpecUtilsV1
|
|||||||
valueToMakeIntoJson = new WidgetBlock(abstractBlockWidgetData);
|
valueToMakeIntoJson = new WidgetBlock(abstractBlockWidgetData);
|
||||||
}
|
}
|
||||||
|
|
||||||
String valueAsJsonString = JsonUtils.toJson(valueToMakeIntoJson, mapper ->
|
///////////////////////////////////////////////
|
||||||
|
// ok now, make the value into a JSON string //
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
String valueAsJsonString;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
|
valueAsJsonString = JsonUtils.toJson(valueToMakeIntoJson, mapper ->
|
||||||
});
|
{
|
||||||
|
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// use this custom serializer to convert null map-keys to empty-strings (rather than having an exception!) //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
mapper.getSerializerProvider().setNullKeySerializer(JsonUtils.nullKeyToEmptyStringSerializer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
LOG.warn("Error deserializing process results with serializationInclusion:ALWAYS - will retry with default settings", e);
|
||||||
|
valueAsJsonString = JsonUtils.toJson(valueToMakeIntoJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// THEN - make it back into a JSONObject or JSONArray, and add it to the valuesAsJsonObject JSONObject //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if(valueAsJsonString.startsWith("["))
|
if(valueAsJsonString.startsWith("["))
|
||||||
{
|
{
|
||||||
valuesAsJsonObject.put(name, new JSONArray(valueAsJsonString));
|
valuesAsJsonObject.put(name, new JSONArray(valueAsJsonString));
|
||||||
|
@ -655,4 +655,28 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase
|
|||||||
assertEquals(200, response.getStatus());
|
assertEquals(200, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test running a process who has a value with a null key.
|
||||||
|
*
|
||||||
|
** This was a regression - that threw an exception from jackson at one point in time.
|
||||||
|
**
|
||||||
|
** Note: ported to v1
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processPutsNullKeyInMap()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_PUTS_NULL_KEY_IN_MAP + "/init").asString();
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertNotNull(jsonObject);
|
||||||
|
JSONObject values = jsonObject.getJSONObject("values");
|
||||||
|
JSONObject mapWithNullKey = values.getJSONObject("mapWithNullKey");
|
||||||
|
assertTrue(mapWithNullKey.has("")); // null key currently set to become empty-string key...
|
||||||
|
assertEquals("hadNullKey", mapWithNullKey.getString(""));
|
||||||
|
assertTrue(mapWithNullKey.has("one"));
|
||||||
|
assertEquals("1", mapWithNullKey.getString("one"));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.javalin;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -53,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceLambda;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||||
@ -112,6 +114,8 @@ public class TestUtils
|
|||||||
public static final String PROCESS_NAME_SIMPLE_THROW = "simpleThrow";
|
public static final String PROCESS_NAME_SIMPLE_THROW = "simpleThrow";
|
||||||
public static final String PROCESS_NAME_SLEEP_INTERACTIVE = "sleepInteractive";
|
public static final String PROCESS_NAME_SLEEP_INTERACTIVE = "sleepInteractive";
|
||||||
|
|
||||||
|
public static final String PROCESS_NAME_PUTS_NULL_KEY_IN_MAP = "putsNullKeyInMap";
|
||||||
|
|
||||||
public static final String STEP_NAME_SLEEPER = "sleeper";
|
public static final String STEP_NAME_SLEEPER = "sleeper";
|
||||||
public static final String STEP_NAME_THROWER = "thrower";
|
public static final String STEP_NAME_THROWER = "thrower";
|
||||||
|
|
||||||
@ -177,6 +181,7 @@ public class TestUtils
|
|||||||
qInstance.addProcess(defineProcessGreetPeopleInteractive());
|
qInstance.addProcess(defineProcessGreetPeopleInteractive());
|
||||||
qInstance.addProcess(defineProcessSimpleSleep());
|
qInstance.addProcess(defineProcessSimpleSleep());
|
||||||
qInstance.addProcess(defineProcessScreenThenSleep());
|
qInstance.addProcess(defineProcessScreenThenSleep());
|
||||||
|
qInstance.addProcess(defineProcessPutsNullKeyInMap());
|
||||||
qInstance.addProcess(defineProcessSimpleThrow());
|
qInstance.addProcess(defineProcessSimpleThrow());
|
||||||
qInstance.addReport(definePersonsReport());
|
qInstance.addReport(definePersonsReport());
|
||||||
qInstance.addPossibleValueSource(definePossibleValueSourcePerson());
|
qInstance.addPossibleValueSource(definePossibleValueSourcePerson());
|
||||||
@ -554,6 +559,26 @@ public class TestUtils
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Define an interactive version of the 'greet people' process
|
||||||
|
*******************************************************************************/
|
||||||
|
private static QProcessMetaData defineProcessPutsNullKeyInMap()
|
||||||
|
{
|
||||||
|
return new QProcessMetaData()
|
||||||
|
.withName(PROCESS_NAME_PUTS_NULL_KEY_IN_MAP)
|
||||||
|
.withTableName(TABLE_NAME_PERSON)
|
||||||
|
.withStep(new QBackendStepMetaData().withName("step")
|
||||||
|
.withCode(new QCodeReferenceLambda<BackendStep>((runBackendStepInput, runBackendStepOutput) ->
|
||||||
|
{
|
||||||
|
HashMap<String, String> mapWithNullKey = new HashMap<>();
|
||||||
|
mapWithNullKey.put(null, "hadNullKey");
|
||||||
|
mapWithNullKey.put("one", "1");
|
||||||
|
runBackendStepOutput.addValue("mapWithNullKey", mapWithNullKey);
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Define a process with just one step that sleeps and then throws
|
** Define a process with just one step that sleeps and then throws
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -216,4 +216,26 @@ class ProcessInitSpecV1Test extends SpecTestBase
|
|||||||
// todo - in a higher-level test, resume test_processInitGoingAsync at the // request job status before sleep is done // line
|
// todo - in a higher-level test, resume test_processInitGoingAsync at the // request job status before sleep is done // line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testProcessPutsNullKeyInMap()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.post(getBaseUrlAndPath() + "/processes/" + TestUtils.PROCESS_NAME_PUTS_NULL_KEY_IN_MAP + "/init")
|
||||||
|
.multiPartContent()
|
||||||
|
.asString();
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
|
||||||
|
JSONObject values = jsonObject.getJSONObject("values");
|
||||||
|
JSONObject mapWithNullKey = values.getJSONObject("mapWithNullKey");
|
||||||
|
assertTrue(mapWithNullKey.has("")); // null key currently set to become empty-string key...
|
||||||
|
assertEquals("hadNullKey", mapWithNullKey.getString(""));
|
||||||
|
assertTrue(mapWithNullKey.has("one"));
|
||||||
|
assertEquals("1", mapWithNullKey.getString("one"));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
Reference in New Issue
Block a user