mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Merge pull request #144 from Kingsrook/feature/hotfix-javalin-process-values-null-map-keys
Feature/hotfix javalin process values null map keys
This commit is contained in:
@ -30,11 +30,14 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@ -54,6 +57,11 @@ public class JsonUtils
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(JsonUtils.class);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// see https://www.baeldung.com/jackson-map-null-values-or-null-key //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
public static NullKeyToEmptyStringSerializer nullKeyToEmptyStringSerializer = new NullKeyToEmptyStringSerializer();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -396,4 +404,41 @@ public class JsonUtils
|
||||
return (record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static class NullKeyToEmptyStringSerializer extends StdSerializer<Object>
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public NullKeyToEmptyStringSerializer()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public NullKeyToEmptyStringSerializer(Class<Object> t)
|
||||
{
|
||||
super(t);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void serialize(Object nullKey, JsonGenerator jsonGenerator, SerializerProvider unused) throws IOException
|
||||
{
|
||||
jsonGenerator.writeFieldName("");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -35,11 +35,13 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@ -318,4 +320,27 @@ class JsonUtilsTest extends BaseTest
|
||||
assertEquals("age", qQueryFilter.getOrderBys().get(0).getFieldName());
|
||||
assertTrue(qQueryFilter.getOrderBys().get(0).getIsAscending());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testNullKeyInMap()
|
||||
{
|
||||
Map<Object, String> mapWithNullKey = MapBuilder.of(null, "foo");
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// assert default behavior throws with null map key //
|
||||
//////////////////////////////////////////////////////
|
||||
assertThatThrownBy(() -> JsonUtils.toJson(mapWithNullKey)).rootCause().hasMessageContaining("Null key for a Map not allowed in JSON");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// assert that the nullKeyToEmptyStringSerializer does what we expect //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
assertEquals("""
|
||||
{"":"foo"}""", JsonUtils.toJson(mapWithNullKey, mapper -> mapper.getSerializerProvider().setNullKeySerializer(JsonUtils.nullKeyToEmptyStringSerializer)));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -352,6 +352,8 @@ public class QJavalinProcessHandler
|
||||
Map<String, Object> resultForCaller = new HashMap<>();
|
||||
Exception returningException = null;
|
||||
|
||||
String processName = context.pathParam("processName");
|
||||
|
||||
try
|
||||
{
|
||||
if(processUUID == null)
|
||||
@ -360,7 +362,6 @@ public class QJavalinProcessHandler
|
||||
}
|
||||
resultForCaller.put("processUUID", processUUID);
|
||||
|
||||
String processName = context.pathParam("processName");
|
||||
LOG.info(startAfterStep == null ? "Initiating process [" + processName + "] [" + processUUID + "]"
|
||||
: "Resuming process [" + processName + "] [" + processUUID + "] after step [" + startAfterStep + "]");
|
||||
|
||||
@ -441,10 +442,30 @@ public class QJavalinProcessHandler
|
||||
// negative side-effects - but be aware. //
|
||||
// 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.Map;
|
||||
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.schemabuilder.SchemaBuilder;
|
||||
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
|
||||
@ -117,6 +118,7 @@ public class ProcessInitOrStepOrStatusResponseV1 implements ProcessInitOrStepOrS
|
||||
** Getter for values
|
||||
**
|
||||
*******************************************************************************/
|
||||
@JsonIgnore // we are doing custom serialization of the values map, so mark as ignore.
|
||||
public Map<String, Serializable> getValues()
|
||||
{
|
||||
return values;
|
||||
|
@ -28,6 +28,7 @@ import java.time.LocalDate;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
@ -141,12 +142,14 @@ public class ProcessSpecUtilsV1
|
||||
errorResponse.setError("Illegal Argument Exception: NaN");
|
||||
errorResponse.setUserFacingError("The process could not be completed due to invalid input.");
|
||||
|
||||
Function<ProcessInitOrStepOrStatusResponseV1, Example> responseToExample = response -> new Example().withValue(convertResponseToJSONObject(response).toMap());
|
||||
|
||||
return MapBuilder.of(() -> new LinkedHashMap<String, Example>())
|
||||
.with("COMPLETE", new Example().withValue(completeResponse))
|
||||
.with("COMPLETE with metaDataAdjustment", new Example().withValue(completeResponseWithMetaDataAdjustment))
|
||||
.with("JOB_STARTED", new Example().withValue(jobStartedResponse))
|
||||
.with("RUNNING", new Example().withValue(runningResponse))
|
||||
.with("ERROR", new Example().withValue(errorResponse))
|
||||
.with("COMPLETE", responseToExample.apply(completeResponse))
|
||||
.with("COMPLETE with metaDataAdjustment", responseToExample.apply(completeResponseWithMetaDataAdjustment))
|
||||
.with("JOB_STARTED", responseToExample.apply(jobStartedResponse))
|
||||
.with("RUNNING", responseToExample.apply(runningResponse))
|
||||
.with("ERROR", responseToExample.apply(errorResponse))
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -155,7 +158,21 @@ public class ProcessSpecUtilsV1
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static void handleOutput(Context context, ProcessInitOrStepOrStatusResponseV1 output)
|
||||
public static void handleOutput(Context context, ProcessInitOrStepOrStatusResponseV1 response)
|
||||
{
|
||||
JSONObject outputJsonObject = convertResponseToJSONObject(response);
|
||||
|
||||
String json = outputJsonObject.toString(3);
|
||||
System.out.println(json);
|
||||
context.result(json);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static JSONObject convertResponseToJSONObject(ProcessInitOrStepOrStatusResponseV1 response)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// normally, we like the JsonUtils behavior of excluding null/empty elements. //
|
||||
@ -163,7 +180,7 @@ public class ProcessSpecUtilsV1
|
||||
// so, go through a loop of object → JSON String → JSONObject → String... //
|
||||
// also - work with the TypedResponse sub-object within this response class //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
ProcessInitOrStepOrStatusResponseV1.TypedResponse typedOutput = output.getTypedResponse();
|
||||
ProcessInitOrStepOrStatusResponseV1.TypedResponse typedOutput = response.getTypedResponse();
|
||||
|
||||
String outputJson = JsonUtils.toJson(typedOutput);
|
||||
JSONObject outputJsonObject = new JSONObject(outputJson);
|
||||
@ -183,6 +200,14 @@ public class ProcessSpecUtilsV1
|
||||
String name = valueEntry.getKey();
|
||||
Serializable value = valueEntry.getValue();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// follow the strategy that we use for JsonUtils.nullKeyToEmptyStringSerializer in this rare case... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(name == null)
|
||||
{
|
||||
name = "";
|
||||
}
|
||||
|
||||
Serializable valueToMakeIntoJson = value;
|
||||
if(value instanceof String s)
|
||||
{
|
||||
@ -213,11 +238,31 @@ public class ProcessSpecUtilsV1
|
||||
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("["))
|
||||
{
|
||||
valuesAsJsonObject.put(name, new JSONArray(valueAsJsonString));
|
||||
@ -252,10 +297,7 @@ public class ProcessSpecUtilsV1
|
||||
outputJsonObject.put("values", valuesAsJsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
String json = outputJsonObject.toString(3);
|
||||
System.out.println(json);
|
||||
context.result(json);
|
||||
return outputJsonObject;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1652,66 +1652,61 @@ paths:
|
||||
examples:
|
||||
COMPLETE:
|
||||
value:
|
||||
typedResponse:
|
||||
nextStep: "reviewScreen"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "COMPLETE"
|
||||
values:
|
||||
totalAge: 32768
|
||||
firstLastName: "Aabramson"
|
||||
values:
|
||||
firstLastName: "Aabramson"
|
||||
totalAge: 32768
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
nextStep: "reviewScreen"
|
||||
type: "COMPLETE"
|
||||
COMPLETE with metaDataAdjustment:
|
||||
value:
|
||||
typedResponse:
|
||||
nextStep: "inputScreen"
|
||||
processMetaDataAdjustment:
|
||||
updatedFields:
|
||||
someField:
|
||||
displayFormat: "%s"
|
||||
isEditable: true
|
||||
isHeavy: false
|
||||
isHidden: false
|
||||
isRequired: true
|
||||
name: "someField"
|
||||
type: "STRING"
|
||||
updatedFrontendStepList:
|
||||
- components:
|
||||
- type: "EDIT_FORM"
|
||||
formFields:
|
||||
- displayFormat: "%s"
|
||||
isEditable: true
|
||||
isHeavy: false
|
||||
isHidden: false
|
||||
isRequired: false
|
||||
name: "someField"
|
||||
type: "STRING"
|
||||
name: "inputScreen"
|
||||
- components:
|
||||
- type: "PROCESS_SUMMARY_RESULTS"
|
||||
name: "resultScreen"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "COMPLETE"
|
||||
values:
|
||||
totalAge: 32768
|
||||
firstLastName: "Aabramson"
|
||||
values:
|
||||
firstLastName: "Aabramson"
|
||||
totalAge: 32768
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
nextStep: "inputScreen"
|
||||
processMetaDataAdjustment:
|
||||
updatedFields:
|
||||
someField:
|
||||
isRequired: true
|
||||
isEditable: true
|
||||
name: "someField"
|
||||
displayFormat: "%s"
|
||||
type: "STRING"
|
||||
isHeavy: false
|
||||
isHidden: false
|
||||
updatedFrontendStepList:
|
||||
- components:
|
||||
- type: "EDIT_FORM"
|
||||
name: "inputScreen"
|
||||
formFields:
|
||||
- isRequired: false
|
||||
isEditable: true
|
||||
name: "someField"
|
||||
displayFormat: "%s"
|
||||
type: "STRING"
|
||||
isHeavy: false
|
||||
isHidden: false
|
||||
- components:
|
||||
- type: "PROCESS_SUMMARY_RESULTS"
|
||||
name: "resultScreen"
|
||||
type: "COMPLETE"
|
||||
JOB_STARTED:
|
||||
value:
|
||||
typedResponse:
|
||||
jobUUID: "98765432-10FE-DCBA-9876-543210FEDCBA"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "JOB_STARTED"
|
||||
jobUUID: "98765432-10FE-DCBA-9876-543210FEDCBA"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "JOB_STARTED"
|
||||
RUNNING:
|
||||
value:
|
||||
typedResponse:
|
||||
current: 47
|
||||
message: "Processing person records"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
total: 1701
|
||||
type: "RUNNING"
|
||||
current: 47
|
||||
total: 1701
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "RUNNING"
|
||||
message: "Processing person records"
|
||||
ERROR:
|
||||
value:
|
||||
typedResponse:
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "RUNNING"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "RUNNING"
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProcessStepResponseV1"
|
||||
description: "State of the initialization of the job, with different fields\
|
||||
@ -1788,66 +1783,61 @@ paths:
|
||||
examples:
|
||||
COMPLETE:
|
||||
value:
|
||||
typedResponse:
|
||||
nextStep: "reviewScreen"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "COMPLETE"
|
||||
values:
|
||||
totalAge: 32768
|
||||
firstLastName: "Aabramson"
|
||||
values:
|
||||
firstLastName: "Aabramson"
|
||||
totalAge: 32768
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
nextStep: "reviewScreen"
|
||||
type: "COMPLETE"
|
||||
COMPLETE with metaDataAdjustment:
|
||||
value:
|
||||
typedResponse:
|
||||
nextStep: "inputScreen"
|
||||
processMetaDataAdjustment:
|
||||
updatedFields:
|
||||
someField:
|
||||
displayFormat: "%s"
|
||||
isEditable: true
|
||||
isHeavy: false
|
||||
isHidden: false
|
||||
isRequired: true
|
||||
name: "someField"
|
||||
type: "STRING"
|
||||
updatedFrontendStepList:
|
||||
- components:
|
||||
- type: "EDIT_FORM"
|
||||
formFields:
|
||||
- displayFormat: "%s"
|
||||
isEditable: true
|
||||
isHeavy: false
|
||||
isHidden: false
|
||||
isRequired: false
|
||||
name: "someField"
|
||||
type: "STRING"
|
||||
name: "inputScreen"
|
||||
- components:
|
||||
- type: "PROCESS_SUMMARY_RESULTS"
|
||||
name: "resultScreen"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "COMPLETE"
|
||||
values:
|
||||
totalAge: 32768
|
||||
firstLastName: "Aabramson"
|
||||
values:
|
||||
firstLastName: "Aabramson"
|
||||
totalAge: 32768
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
nextStep: "inputScreen"
|
||||
processMetaDataAdjustment:
|
||||
updatedFields:
|
||||
someField:
|
||||
isRequired: true
|
||||
isEditable: true
|
||||
name: "someField"
|
||||
displayFormat: "%s"
|
||||
type: "STRING"
|
||||
isHeavy: false
|
||||
isHidden: false
|
||||
updatedFrontendStepList:
|
||||
- components:
|
||||
- type: "EDIT_FORM"
|
||||
name: "inputScreen"
|
||||
formFields:
|
||||
- isRequired: false
|
||||
isEditable: true
|
||||
name: "someField"
|
||||
displayFormat: "%s"
|
||||
type: "STRING"
|
||||
isHeavy: false
|
||||
isHidden: false
|
||||
- components:
|
||||
- type: "PROCESS_SUMMARY_RESULTS"
|
||||
name: "resultScreen"
|
||||
type: "COMPLETE"
|
||||
JOB_STARTED:
|
||||
value:
|
||||
typedResponse:
|
||||
jobUUID: "98765432-10FE-DCBA-9876-543210FEDCBA"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "JOB_STARTED"
|
||||
jobUUID: "98765432-10FE-DCBA-9876-543210FEDCBA"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "JOB_STARTED"
|
||||
RUNNING:
|
||||
value:
|
||||
typedResponse:
|
||||
current: 47
|
||||
message: "Processing person records"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
total: 1701
|
||||
type: "RUNNING"
|
||||
current: 47
|
||||
total: 1701
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "RUNNING"
|
||||
message: "Processing person records"
|
||||
ERROR:
|
||||
value:
|
||||
typedResponse:
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "RUNNING"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "RUNNING"
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProcessStepResponseV1"
|
||||
description: "State of the backend's running of the next step(s) of the\
|
||||
@ -1895,66 +1885,61 @@ paths:
|
||||
examples:
|
||||
COMPLETE:
|
||||
value:
|
||||
typedResponse:
|
||||
nextStep: "reviewScreen"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "COMPLETE"
|
||||
values:
|
||||
totalAge: 32768
|
||||
firstLastName: "Aabramson"
|
||||
values:
|
||||
firstLastName: "Aabramson"
|
||||
totalAge: 32768
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
nextStep: "reviewScreen"
|
||||
type: "COMPLETE"
|
||||
COMPLETE with metaDataAdjustment:
|
||||
value:
|
||||
typedResponse:
|
||||
nextStep: "inputScreen"
|
||||
processMetaDataAdjustment:
|
||||
updatedFields:
|
||||
someField:
|
||||
displayFormat: "%s"
|
||||
isEditable: true
|
||||
isHeavy: false
|
||||
isHidden: false
|
||||
isRequired: true
|
||||
name: "someField"
|
||||
type: "STRING"
|
||||
updatedFrontendStepList:
|
||||
- components:
|
||||
- type: "EDIT_FORM"
|
||||
formFields:
|
||||
- displayFormat: "%s"
|
||||
isEditable: true
|
||||
isHeavy: false
|
||||
isHidden: false
|
||||
isRequired: false
|
||||
name: "someField"
|
||||
type: "STRING"
|
||||
name: "inputScreen"
|
||||
- components:
|
||||
- type: "PROCESS_SUMMARY_RESULTS"
|
||||
name: "resultScreen"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "COMPLETE"
|
||||
values:
|
||||
totalAge: 32768
|
||||
firstLastName: "Aabramson"
|
||||
values:
|
||||
firstLastName: "Aabramson"
|
||||
totalAge: 32768
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
nextStep: "inputScreen"
|
||||
processMetaDataAdjustment:
|
||||
updatedFields:
|
||||
someField:
|
||||
isRequired: true
|
||||
isEditable: true
|
||||
name: "someField"
|
||||
displayFormat: "%s"
|
||||
type: "STRING"
|
||||
isHeavy: false
|
||||
isHidden: false
|
||||
updatedFrontendStepList:
|
||||
- components:
|
||||
- type: "EDIT_FORM"
|
||||
name: "inputScreen"
|
||||
formFields:
|
||||
- isRequired: false
|
||||
isEditable: true
|
||||
name: "someField"
|
||||
displayFormat: "%s"
|
||||
type: "STRING"
|
||||
isHeavy: false
|
||||
isHidden: false
|
||||
- components:
|
||||
- type: "PROCESS_SUMMARY_RESULTS"
|
||||
name: "resultScreen"
|
||||
type: "COMPLETE"
|
||||
JOB_STARTED:
|
||||
value:
|
||||
typedResponse:
|
||||
jobUUID: "98765432-10FE-DCBA-9876-543210FEDCBA"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "JOB_STARTED"
|
||||
jobUUID: "98765432-10FE-DCBA-9876-543210FEDCBA"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "JOB_STARTED"
|
||||
RUNNING:
|
||||
value:
|
||||
typedResponse:
|
||||
current: 47
|
||||
message: "Processing person records"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
total: 1701
|
||||
type: "RUNNING"
|
||||
current: 47
|
||||
total: 1701
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "RUNNING"
|
||||
message: "Processing person records"
|
||||
ERROR:
|
||||
value:
|
||||
typedResponse:
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "RUNNING"
|
||||
processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF"
|
||||
type: "RUNNING"
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProcessStepResponseV1"
|
||||
description: "State of the backend's running of the specified job, with\
|
||||
|
@ -655,4 +655,28 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase
|
||||
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.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
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.authentication.QAuthenticationMetaData;
|
||||
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.dashboard.QWidgetMetaData;
|
||||
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_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_THROWER = "thrower";
|
||||
|
||||
@ -177,6 +181,7 @@ public class TestUtils
|
||||
qInstance.addProcess(defineProcessGreetPeopleInteractive());
|
||||
qInstance.addProcess(defineProcessSimpleSleep());
|
||||
qInstance.addProcess(defineProcessScreenThenSleep());
|
||||
qInstance.addProcess(defineProcessPutsNullKeyInMap());
|
||||
qInstance.addProcess(defineProcessSimpleThrow());
|
||||
qInstance.addReport(definePersonsReport());
|
||||
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
|
||||
*******************************************************************************/
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@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