diff --git a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/BpmProviderFlowable.java b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/BpmProviderFlowable.java index 2edced73ac2..e0da2887db7 100644 --- a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/BpmProviderFlowable.java +++ b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/BpmProviderFlowable.java @@ -24,7 +24,6 @@ import java.util.Set; import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; -import org.eclipse.dirigible.commons.api.helpers.GsonHelper; import org.eclipse.dirigible.components.base.tenant.Tenant; import org.eclipse.dirigible.components.base.tenant.TenantContext; import org.eclipse.dirigible.components.engine.bpm.BpmProvider; @@ -174,10 +173,7 @@ public void undeployProcess(String deploymentId) { * @return the process instance id */ public String startProcess(String key, String businessKey, String parameters) { - @SuppressWarnings("unchecked") - Map variables = GsonHelper.fromJson(parameters, HashMap.class); - - return startProcess(key, businessKey, variables); + return startProcess(key, businessKey, ProcessVariables.fromJson(parameters)); } public String startProcess(String key, String businessKey, Map variables) { diff --git a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/ProcessVariables.java b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/ProcessVariables.java new file mode 100644 index 00000000000..b45b188f5c2 --- /dev/null +++ b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/ProcessVariables.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.engine.bpm.flowable.config; + +import java.util.Map; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.ToNumberPolicy; +import com.google.gson.reflect.TypeToken; + +/** + * Reads a JSON document of process variables, keeping a whole number whole (issue #7373). + *

+ * The default Gson deserialization of an untyped JSON number is a {@code Double}, so a record's + * primary key handed to {@code Process.start} as {@code 33} landed in Flowable's variable store as + * {@code 33.0} - and every reader that renders such a variable as text says so: an Inbox row's + * subject locator read {@code "33.0"}, which the generated controller's integer path parameter then + * refused, so the card could not load the very record the task is about. A JSON number with no + * fractional part is an integer, so it is read as a {@code Long}; a genuine decimal stays a + * {@code Double}. + */ +final class ProcessVariables { + + /** Variables are untyped, so the number strategy is what decides Long versus Double. */ + private static final Gson GSON = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .create(); + + private static final TypeToken> VARIABLES = new TypeToken<>() {}; + + private ProcessVariables() {} + + /** + * Parses a JSON document whose top-level fields are process variables. + * + * @param json the document, may be null + * @return the variables, or null when there is no document + */ + static Map fromJson(String json) { + return GSON.fromJson(json, VARIABLES); + } +} diff --git a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/TaskServiceImpl.java b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/TaskServiceImpl.java index 8ba2298298e..b5c81e42d3d 100644 --- a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/TaskServiceImpl.java +++ b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/TaskServiceImpl.java @@ -183,8 +183,7 @@ public void setTaskVariable(String taskId, String variableName, Object variable) @Override public void completeTask(String taskId, String variables) { - Map processVariables = GsonHelper.fromJson(variables, HashMap.class); - completeTask(taskId, processVariables); + completeTask(taskId, ProcessVariables.fromJson(variables)); } @Override diff --git a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/dto/TaskSubject.java b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/dto/TaskSubject.java index e51665aecf1..ce37c4bb80a 100644 --- a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/dto/TaskSubject.java +++ b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/dto/TaskSubject.java @@ -68,7 +68,7 @@ public record Field(String property, String kind, String url, String label) { public static TaskSubject from(Map variables) { String declaration = text(variables.get(SUBJECT_FIELDS)); String url = text(variables.get(ENTITY_URL)); - String id = text(variables.get(ENTITY_ID)); + String id = key(variables.get(ENTITY_ID)); if (declaration == null || url == null || id == null) { return null; } @@ -101,6 +101,26 @@ public static TaskSubject from(Map variables) { return fields.isEmpty() ? null : new TaskSubject(url, id, fields); } + /** + * The record's key, as the whole number it is (issue #7373). An instance started before the + * variable store stopped widening an untyped JSON number to a {@code Double} carries the key as + * {@code 33.0}, and the generated controller's integer path parameter refuses that - so the card + * could not load the record it exists to show. Anything that is not an integral number is read as + * it stands. + * + * @param value the raw {@code __entityId} variable + * @return the key as text, or null when there is none + */ + private static String key(Object value) { + if (value instanceof Double || value instanceof Float) { + double raw = ((Number) value).doubleValue(); + if (Double.isFinite(raw) && raw == Math.rint(raw)) { + return String.valueOf((long) raw); + } + } + return text(value); + } + private static String text(Object value) { if (value == null) { return null; diff --git a/components/engine/engine-bpm-flowable/src/test/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/ProcessVariablesEngineTest.java b/components/engine/engine-bpm-flowable/src/test/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/ProcessVariablesEngineTest.java new file mode 100644 index 00000000000..b995d76120d --- /dev/null +++ b/components/engine/engine-bpm-flowable/src/test/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/ProcessVariablesEngineTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.engine.bpm.flowable.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.dirigible.components.engine.bpm.flowable.dto.TaskSubject; +import org.flowable.engine.ProcessEngine; +import org.flowable.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration; +import org.flowable.task.api.Task; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * That a key started as a JSON number survives Flowable's variable store as the whole number it is, + * against a REAL engine (issue #7373): the store types the variable from the value it is handed, so + * a {@code Double} there is what every reader downstream renders as {@code "33.0"} - the locator an + * Inbox row's subject card then cannot load the record through. + */ +class ProcessVariablesEngineTest { + + private static final String PROCESS_XML = + """ + + + + + + + + + + + """; + + /** Exactly what a generated trigger hands Process.start - Json.stringify of its locator map. */ + private static final String START_PAYLOAD = """ + {"Id": 33, "__entityUrl": "/services/java/billing/gen/billing/api/sales_invoice/SalesInvoiceController", + "__entityId": 33, "__subjectFields": "Number:text,Total:number"} + """; + + private static ProcessEngine engine; + + @BeforeAll + static void startEngine() { + StandaloneInMemProcessEngineConfiguration configuration = new StandaloneInMemProcessEngineConfiguration(); + configuration.setJdbcUrl("jdbc:h2:mem:process-variables-test;DB_CLOSE_DELAY=-1"); + engine = configuration.buildProcessEngine(); + engine.getRepositoryService() + .createDeployment() + .addString("approve.bpmn20.xml", PROCESS_XML) + .deploy(); + engine.getRuntimeService() + .startProcessInstanceByKey("approve", ProcessVariables.fromJson(START_PAYLOAD)); + } + + @AfterAll + static void stopEngine() { + if (engine != null) { + engine.close(); + } + } + + @Test + void theRecordsKeyIsReadableAsAnIntegerPathParameter() { + Task task = engine.getTaskService() + .createTaskQuery() + .taskAssignee("admin") + .includeProcessVariables() + .singleResult(); + + assertEquals(33L, task.getProcessVariables() + .get("__entityId")); + assertEquals("33", TaskSubject.from(task.getProcessVariables()) + .id()); + } +} diff --git a/components/engine/engine-bpm-flowable/src/test/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/ProcessVariablesTest.java b/components/engine/engine-bpm-flowable/src/test/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/ProcessVariablesTest.java new file mode 100644 index 00000000000..02ed4a03bbe --- /dev/null +++ b/components/engine/engine-bpm-flowable/src/test/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/ProcessVariablesTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.engine.bpm.flowable.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +/** + * Reading the JSON a process is started and a task is completed with (issue #7373). + */ +class ProcessVariablesTest { + + /** + * The defect itself: the record's key reached the variable store as a Double, so every reader that + * renders it as text said "33.0" - which the generated controller's integer path parameter refuses. + */ + @Test + void aWholeNumberStaysWhole() { + Map variables = ProcessVariables.fromJson("{\"__entityId\": 33, \"Id\": 33}"); + + assertEquals(33L, variables.get("__entityId")); + assertEquals("33", String.valueOf(variables.get("Id"))); + } + + /** A genuine decimal keeps its fraction - only the widening of an integer was wrong. */ + @Test + void aDecimalStaysADecimal() { + Map variables = ProcessVariables.fromJson("{\"Total\": 12.5, \"Name\": \"ACME\", \"Draft\": true}"); + + assertEquals(12.5d, variables.get("Total")); + assertEquals("ACME", variables.get("Name")); + assertEquals(Boolean.TRUE, variables.get("Draft")); + } + + /** A start with no parameters at all is a start with no variables, as before. */ + @Test + void noDocumentIsNoVariables() { + assertNull(ProcessVariables.fromJson(null)); + } +} diff --git a/components/engine/engine-bpm-flowable/src/test/java/org/eclipse/dirigible/components/engine/bpm/flowable/dto/TaskSubjectTest.java b/components/engine/engine-bpm-flowable/src/test/java/org/eclipse/dirigible/components/engine/bpm/flowable/dto/TaskSubjectTest.java index cf3c4fc11c8..e8382f4357c 100644 --- a/components/engine/engine-bpm-flowable/src/test/java/org/eclipse/dirigible/components/engine/bpm/flowable/dto/TaskSubjectTest.java +++ b/components/engine/engine-bpm-flowable/src/test/java/org/eclipse/dirigible/components/engine/bpm/flowable/dto/TaskSubjectTest.java @@ -65,6 +65,20 @@ void aRelationWithoutItsLocatorIsDropped() { .size()); } + /** + * An instance started before the variable store stopped widening an untyped JSON number carries the + * key as a Double; the locator must still be the integer the generated controller's path parameter + * accepts, or the card cannot load the record (issue #7373). + */ + @Test + void aKeyWidenedToADoubleStillReadsAsAWholeNumber() { + Map variables = variables(); + variables.put("__entityId", 33.0d); + + assertEquals("33", TaskSubject.from(variables) + .id()); + } + /** A hand-authored BPMN, or a deployment generated before #7077, declares no subject at all. */ @Test void aProcessThatDeclaresNoSubjectHasNone() { diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java index 744b2fdebf3..6a87507c241 100644 --- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java +++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java @@ -5312,7 +5312,13 @@ private void assertRuntimeEnforcement() { .then() .statusCode(200) .body("find { it.name == 'Confirm' }.subject.url", notNullValue()) - .body("find { it.name == 'Confirm' }.subject.fields.property", hasItem("Name")), + .body("find { it.name == 'Confirm' }.subject.fields.property", hasItem("Name")) + // ...and the record's key is the WHOLE number the generated controller's + // integer path parameter accepts (#7373): started as an untyped JSON + // number it used to reach the variable store as a Double, so the locator + // read "1.0" and the card could not load the record it exists to show. + .body("find { it.name == 'Confirm' }.subject.id", + org.hamcrest.Matchers.matchesPattern("\\d+")), 30); // ...and the record knows about the instance that task belongs to: the trigger stamped the