Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, Object> 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<String, Object> variables) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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).
* <p>
* 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<Map<String, Object>> 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<String, Object> fromJson(String json) {
return GSON.fromJson(json, VARIABLES);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,7 @@ public void setTaskVariable(String taskId, String variableName, Object variable)

@Override
public void completeTask(String taskId, String variables) {
Map<String, Object> processVariables = GsonHelper.fromJson(variables, HashMap.class);
completeTask(taskId, processVariables);
completeTask(taskId, ProcessVariables.fromJson(variables));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public record Field(String property, String kind, String url, String label) {
public static TaskSubject from(Map<String, Object> 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;
}
Expand Down Expand Up @@ -101,6 +101,26 @@ public static TaskSubject from(Map<String, Object> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 =
"""
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:flowable="http://flowable.org/bpmn" targetNamespace="http://www.flowable.org/processdef">
<process id="approve" name="Approve" isExecutable="true">
<startEvent id="start"></startEvent>
<userTask id="approveTask" name="Approve" flowable:assignee="admin"></userTask>
<endEvent id="end"></endEvent>
<sequenceFlow id="flow_start_task" sourceRef="start" targetRef="approveTask"></sequenceFlow>
<sequenceFlow id="flow_task_end" sourceRef="approveTask" targetRef="end"></sequenceFlow>
</process>
</definitions>
""";

/** 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());
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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<String, Object> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading