From 1d0748574624c5fc54e8269830f74d9222883f13 Mon Sep 17 00:00:00 2001 From: Andreas Isler Date: Fri, 12 Jun 2026 10:13:27 +0200 Subject: [PATCH] explicit parameters, changed array nodes in xls files from * to [] --- .../work/SecurityHttpBasicConfiguration.java | 14 +- .../work/StaticResourceConfiguration.java | 4 +- .../work/service/TaskProcessStarter.java | 2 - .../com/customer/work/service/VarUtils.java | 4 +- .../work/model/FlowableExcelMapper.java | 438 +++++++----------- .../work/model/FlowableExcelParser.java | 124 ++--- .../work/model/FlowableModelTestUtils.java | 165 ++++--- .../customer/work/model/TestMailServer.java | 10 +- .../customer/work/model/test/ModelTest.java | 20 +- .../resources/model/test/P002/boolean1.json | 6 +- .../resources/model/test/P002/boolean2.json | 6 +- .../test/resources/model/test/P002/date.json | 6 +- .../resources/model/test/P002/double.json | 6 +- .../test/resources/model/test/P002/int.json | 6 +- .../resources/model/test/P002/p002Test.xlsx | Bin 22656 -> 22359 bytes .../resources/model/test/P002/string1.json | 6 +- .../resources/model/test/P002/string2.json | 6 +- .../resources/model/test/P002/string3.json | 6 +- .../model/test/TrackVars/trackVars.xlsx | Bin 19606 -> 19622 bytes .../test/resources/model/test/implicit.json | 6 + 20 files changed, 358 insertions(+), 477 deletions(-) create mode 100644 customer-work/src/test/resources/model/test/implicit.json diff --git a/customer-work/src/main/java/com/customer/work/SecurityHttpBasicConfiguration.java b/customer-work/src/main/java/com/customer/work/SecurityHttpBasicConfiguration.java index 2786cf5..f304862 100644 --- a/customer-work/src/main/java/com/customer/work/SecurityHttpBasicConfiguration.java +++ b/customer-work/src/main/java/com/customer/work/SecurityHttpBasicConfiguration.java @@ -1,9 +1,10 @@ package com.customer.work; -import java.util.stream.Collectors; - +import com.flowable.autoconfigure.security.FlowableHttpSecurityCustomizer; +import com.flowable.autoconfigure.security.servlet.PlatformPathRequest; +import com.flowable.core.spring.security.web.authentication.AjaxAuthenticationFailureHandler; +import com.flowable.core.spring.security.web.authentication.AjaxAuthenticationSuccessHandler; import jakarta.servlet.DispatcherType; - import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; @@ -18,10 +19,7 @@ import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher; -import com.flowable.autoconfigure.security.FlowableHttpSecurityCustomizer; -import com.flowable.autoconfigure.security.servlet.PlatformPathRequest; -import com.flowable.core.spring.security.web.authentication.AjaxAuthenticationFailureHandler; -import com.flowable.core.spring.security.web.authentication.AjaxAuthenticationSuccessHandler; +import java.util.stream.Collectors; @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "application.security", name = "type", havingValue = "basic", matchIfMissing = true) @@ -32,7 +30,7 @@ public class SecurityHttpBasicConfiguration { @Order(10) public SecurityFilterChain basicDefaultSecurity(HttpSecurity http, ObjectProvider httpSecurityCustomizers) throws Exception { for (FlowableHttpSecurityCustomizer customizer : httpSecurityCustomizers.orderedStream() - .collect(Collectors.toList())) { + .toList()) { customizer.customize(http); } diff --git a/customer-work/src/main/java/com/customer/work/StaticResourceConfiguration.java b/customer-work/src/main/java/com/customer/work/StaticResourceConfiguration.java index 622a27d..1c5c88f 100644 --- a/customer-work/src/main/java/com/customer/work/StaticResourceConfiguration.java +++ b/customer-work/src/main/java/com/customer/work/StaticResourceConfiguration.java @@ -1,12 +1,12 @@ package com.customer.work; -import java.util.concurrent.TimeUnit; - import org.springframework.context.annotation.Configuration; import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.concurrent.TimeUnit; + @Configuration(proxyBeanMethods = false) public class StaticResourceConfiguration implements WebMvcConfigurer { diff --git a/customer-work/src/main/java/com/customer/work/service/TaskProcessStarter.java b/customer-work/src/main/java/com/customer/work/service/TaskProcessStarter.java index c327174..f39e71c 100644 --- a/customer-work/src/main/java/com/customer/work/service/TaskProcessStarter.java +++ b/customer-work/src/main/java/com/customer/work/service/TaskProcessStarter.java @@ -14,13 +14,11 @@ public class TaskProcessStarter { private RuntimeService runtimeService; public void start(String processKey, DelegateTask task) { -// String processKey = (String) task.getVariable("subProcessKey"); // key from variable Map vars = new HashMap<>(task.getVariables()); // carry over existing vars vars.put("originTaskId", task.getId()); vars.put("originTaskName", task.getName()); runtimeService.startProcessInstanceByKey(processKey, vars); -// runtimeService.startProcessInstanceById(processKey, vars); } } diff --git a/customer-work/src/main/java/com/customer/work/service/VarUtils.java b/customer-work/src/main/java/com/customer/work/service/VarUtils.java index cc90f6a..d414d85 100644 --- a/customer-work/src/main/java/com/customer/work/service/VarUtils.java +++ b/customer-work/src/main/java/com/customer/work/service/VarUtils.java @@ -7,14 +7,14 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.flowable.cmmn.api.CmmnRuntimeService; +import org.flowable.cmmn.api.runtime.CaseInstance; +import org.flowable.cmmn.api.runtime.PlanItemInstance; import org.flowable.cmmn.engine.CmmnEngineConfiguration; import org.flowable.cmmn.engine.impl.persistence.entity.CaseInstanceEntity; import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent; import org.flowable.common.engine.api.delegate.event.FlowableEvent; import org.flowable.common.engine.api.delegate.event.FlowableEventListener; import org.flowable.engine.ProcessEngineConfiguration; -import org.flowable.cmmn.api.runtime.CaseInstance; -import org.flowable.cmmn.api.runtime.PlanItemInstance; import org.flowable.engine.RuntimeService; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.impl.persistence.entity.ExecutionEntity; diff --git a/customer-work/src/test/java/com/customer/work/model/FlowableExcelMapper.java b/customer-work/src/test/java/com/customer/work/model/FlowableExcelMapper.java index b6baa2c..256f748 100644 --- a/customer-work/src/test/java/com/customer/work/model/FlowableExcelMapper.java +++ b/customer-work/src/test/java/com/customer/work/model/FlowableExcelMapper.java @@ -4,332 +4,218 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.stereotype.Component; -import java.time.*; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; - +/** + * Maps an Excel workbook (created with Excel or LibreOffice) to test data rows. + * Sheet layout: + * - The 1st row of every sheet contains the path of each variable. + * Object fields are separated by ".", array elements are addressed with "[index]", + * e.g. "root.param[0]" or "root.items[1].name". + * - The 2nd row contains the type of each variable: + * String, Boolean, Integer, Long, Double, Date + * - All other rows contain the values. Empty cells are skipped. + * Special String values: __NULL, __N_A, __EMPTY, __BLANK + */ @Component public class FlowableExcelMapper { - protected static ObjectMapper objectMapper = new ObjectMapper(); - protected static JavaTimeModule javaTimeModule = new JavaTimeModule(); - protected static FlowableExcelParser flowableExcelParser = new FlowableExcelParser(); + protected final ObjectMapper objectMapper = new ObjectMapper(); + protected final FlowableExcelParser flowableExcelParser; - public FlowableExcelMapper() { + protected static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + protected static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + public FlowableExcelMapper(FlowableExcelParser flowableExcelParser) { + this.flowableExcelParser = flowableExcelParser; // Enable ObjectMapper for handling Instant as string - objectMapper.registerModule(javaTimeModule); + objectMapper.registerModule(new JavaTimeModule()); objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); } - // excelPath defines the location of the Excel file - // FlowableExcelParser is used to create an ArrayList from the Excel file - // The 1st row of every sheet contains the path of the variables to be created in the JsonNode - // Delimiter is "." for an ObjectNode and "*" for an ArrayNode - // The 2nd row contains tht type of the variable. Supported types are: - // - String - // - Boolean - // - Integer - // - Double - // - Date - // All sheets are added to an ArrayNode - // All rows are added to an ArrayNode in the sheets ArrayNode - // All cell values are added to an ObjectNode in the rows ArrayNode + // Book as JSON: one ArrayNode per sheet, one node per data row public ArrayNode excelBookResourceToJsonNode(String resourcePath) { - ArrayList>> book = flowableExcelParser.parseExcelBookFromResource(resourcePath); - return bookToJsonNode(book); - } - - protected ArrayNode bookToJsonNode(ArrayList>> book) { ArrayNode bookNode = objectMapper.createArrayNode(); - for (ArrayList> sheet : book) { - bookNode.add(sheetToJsonNode(sheet)); + for (List> sheet : excelBookResourceToObj(resourcePath)) { + ArrayNode sheetNode = objectMapper.createArrayNode(); + for (Map row : sheet) { + sheetNode.add(objectMapper.valueToTree(row)); + } + bookNode.add(sheetNode); } return bookNode; } - protected ArrayNode sheetToJsonNode(ArrayList> rows) { - ArrayList paths = new ArrayList<>(); - ArrayList types = new ArrayList<>(); - ArrayNode arrayNode = objectMapper.createArrayNode(); - for (int rowNum = 0; rowNum < rows.size(); rowNum++) { - if (rowNum == 0) { - // The 1st row contains the paths - paths.addAll(rows.get(rowNum)); - } else if (rowNum == 1) { - // The 2nd row contains the types of the variables - types.addAll(rows.get(rowNum)); - } else { - // All other rows contain the values of the variables - arrayNode.add(rowToJsonNode(types, paths, rows.get(rowNum), null)); - } + // Book as plain Java objects: one list per sheet, one map per data row + public List>> excelBookResourceToObj(String resourcePath) { + List>> book = new ArrayList<>(); + for (List> sheet : flowableExcelParser.parseExcelBookFromResource(resourcePath)) { + book.add(sheetToRows(sheet)); } - return arrayNode; + return book; } - protected JsonNode rowToJsonNode(ArrayList types, ArrayList paths, ArrayList row, JsonNode jsonNode) { + protected List> sheetToRows(List> rows) { + List> dataRows = new ArrayList<>(); + if (rows.size() < 2) { + // A sheet needs at least the path row and the type row to contain data + return dataRows; + } + List paths = rows.get(0); + List types = rows.get(1); + for (int rowNum = 2; rowNum < rows.size(); rowNum++) { + dataRows.add(rowToMap(paths, types, rows.get(rowNum))); + } + return dataRows; + } + + protected Map rowToMap(List paths, List types, List row) { + Map rowMap = new LinkedHashMap<>(); for (int cellNum = 0; cellNum < row.size(); cellNum++) { String cellValue = row.get(cellNum); - // Empty cells are not added to node + // Empty cells are not added if (cellValue != null && !cellValue.isEmpty()) { - String type = types.get(cellNum); - Object content; + Object content = parseCellValue(types.get(cellNum), cellValue); + setValue(rowMap, parsePath(paths.get(cellNum)), content); + } + } + return rowMap; + } + + protected Object parseCellValue(String type, String cellValue) { + switch (cellValue) { + case "__NULL": return null; + case "__N_A": return "__N_A"; + } + switch (type) { + case "String": switch (cellValue) { - case "__NULL": content = null; break; - case "__N_A": content = "__N_A"; break; - default: - switch (type) { - case "String": - switch (cellValue) { - case "__EMPTY": content = ""; break; - case "__BLANK": content = " "; break; - default: content = cellValue; - } - break; - case "Boolean": content = Boolean.parseBoolean(cellValue); break; - case "Integer": content = Integer.parseInt(cellValue); break; - case "Double": content = Double.parseDouble(cellValue); break; - case "Long": content = Long.parseLong(cellValue); break; - case "Date": - // Excel considers dates as local dates, but we want them as UTC - Instant instant; - try { - LocalDateTime localDateTime = LocalDateTime.parse(cellValue, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant(); - } catch (DateTimeParseException e) { - LocalDate localDate = LocalDate.parse(cellValue, DateTimeFormatter.ofPattern("yyyy-MM-dd")); - instant = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); - } - content = instant.plusSeconds(ZonedDateTime.now().getOffset().getTotalSeconds()); - break; - default: - throw new RuntimeException("Variable type not supported: " + type); - } + case "__EMPTY": return ""; + case "__BLANK": return " "; + default: return cellValue; } - jsonNode = addNode(jsonNode, paths.get(cellNum), content); - } + case "Boolean": return Boolean.parseBoolean(cellValue); + case "Integer": return Integer.parseInt(cellValue); + case "Long": return Long.parseLong(cellValue); + case "Double": return Double.parseDouble(cellValue); + case "Date": return parseDate(cellValue); + default: + throw new IllegalArgumentException("Variable type not supported: " + type); } - return jsonNode; } - protected JsonNode addNode(JsonNode parent, String path, Object value) { - if (parent == null) { - parent = objectMapper.createObjectNode(); + protected Instant parseDate(String cellValue) { + // Excel and LibreOffice consider dates as local dates, the tests treat them as UTC + try { + return LocalDateTime.parse(cellValue, DATE_TIME_FORMAT).toInstant(ZoneOffset.UTC); + } catch (DateTimeParseException e) { + return LocalDate.parse(cellValue, DATE_FORMAT).atStartOfDay(ZoneOffset.UTC).toInstant(); } + } - int delimiterPosition = indexOfFirstDelimiter(path, ".*"); - if (delimiterPosition <= 0) { - if (parent instanceof ObjectNode) { - // Key of a value in an ObjectNode - ((ObjectNode) parent).set(path, objectMapper.valueToTree(value)); + /** + * Splits a path like "root.items[1].name" into the tokens "root", "items", 1, "name". + * Field names are Strings, array indexes are Integers. + */ + protected List parsePath(String path) { + List tokens = new ArrayList<>(); + int pos = 0; + while (pos < path.length()) { + char c = path.charAt(pos); + if (c == '[') { + int end = path.indexOf(']', pos); + if (end < 0) { + throw new IllegalArgumentException("Missing ']' in path: " + path); + } + try { + tokens.add(Integer.parseInt(path.substring(pos + 1, end))); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid array index in path: " + path, e); + } + pos = end + 1; + // An index may be followed by ".field", another "[index]" or the end of the path + if (pos < path.length() && path.charAt(pos) == '.') { + pos++; + } } else { - // Index of a value in an ArrayNode - int index = Integer.parseInt(path); - while (parent.size() <= index) { - // Create empty entries to parent ArrayNode - ((ArrayNode) parent).add(objectMapper.createObjectNode()); + if (c == '.') { + pos++; } - ((ArrayNode) parent).set(index, objectMapper.valueToTree(value)); - } - return parent; - } - - String key = path.substring(0, delimiterPosition); - String newPath = path.substring(delimiterPosition + 1); - String delimiter = path.substring(delimiterPosition, delimiterPosition + 1); - JsonNode child = null; - if (delimiter.equals(".")) { - child = objectMapper.createObjectNode(); - } else if (delimiter.equals("*")) { - child = objectMapper.createArrayNode(); - } - - JsonNode node; - if (parent instanceof ObjectNode) { - // Key of a JsonNode in an ObjectNode - node = parent.get(key); - if (node != null) { - // Take the existing ObjectNode as child - child = node; - } - ((ObjectNode) parent).set(key, addNode(child, newPath, value)); - } else { - // Index of a JsonNode in an ArrayNode - int index = Integer.parseInt(key); - node = parent.get(index); - if (node != null) { - // Take the existing ArrayNode as child - child = node; - } - while (parent.size() <= index) { - // Create empty entries to parent ArrayNode - ((ArrayNode) parent).add(objectMapper.createObjectNode()); - } - ((ArrayNode) parent).set(index, addNode(child, newPath, value)); - } - return parent; - } - - protected int indexOfFirstDelimiter(String str, String delimiters) { - for (int i = 0; i < str.length(); i++) { - for (char c : delimiters.toCharArray()) { - if (str.charAt(i) == c) { - return i; + int end = pos; + while (end < path.length() && path.charAt(end) != '.' && path.charAt(end) != '[') { + end++; } - } - } - return -1; - } - - public ArrayList> excelBookResourceToObj(String resourcePath) { - ArrayList>> book = flowableExcelParser.parseExcelBookFromResource(resourcePath); - return bookToObj(book); - } - - protected ArrayList> bookToObj(ArrayList>> book) { - ArrayList> bookNode = new ArrayList<>(); - for (ArrayList> sheet : book) { - bookNode.add(sheetToObj(sheet)); - } - return bookNode; - } - - protected ArrayList sheetToObj(ArrayList> rows) { - ArrayList paths = new ArrayList<>(); - ArrayList types = new ArrayList<>(); - ArrayList arrayNode = new ArrayList<>(); - for (int rowNum = 0; rowNum < rows.size(); rowNum++) { - if (rowNum == 0) { - // The 1st row contains the paths - paths.addAll(rows.get(rowNum)); - } else if (rowNum == 1) { - // The 2nd row contains the types of the variables - types.addAll(rows.get(rowNum)); - } else { - // All other rows contain the values of the variables - arrayNode.add(rowToObj(types, paths, rows.get(rowNum), null)); - } - } - return arrayNode; - } - - protected Object rowToObj(ArrayList types, ArrayList paths, ArrayList row, Object node) { - for (int cellNum = 0; cellNum < row.size(); cellNum++) { - String cellValue = row.get(cellNum); - // Empty cells are not added to node - if (cellValue != null && !cellValue.isEmpty()) { - String type = types.get(cellNum); - Object content; - switch (cellValue) { - case "__NULL": content = null; break; - case "__N_A": content = "__N_A"; break; - default: - switch (type) { - case "String": - switch (cellValue) { - case "__EMPTY": content = ""; break; - case "__BLANK": content = " "; break; - default: content = cellValue; - } - break; - case "Boolean": content = Boolean.parseBoolean(cellValue); break; - case "Integer": content = Integer.parseInt(cellValue); break; - case "Double": content = Double.parseDouble(cellValue); break; - case "Long": content = Long.parseLong(cellValue); break; - case "Date": - // Excel considers dates as local dates, but we want them as UTC - Instant instant; - try { - LocalDateTime localDateTime = LocalDateTime.parse(cellValue, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant(); - } catch (DateTimeParseException e) { - LocalDate localDate = LocalDate.parse(cellValue, DateTimeFormatter.ofPattern("yyyy-MM-dd")); - instant = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); - } - content = instant.plusSeconds(ZonedDateTime.now().getOffset().getTotalSeconds()); - break; - default: - throw new RuntimeException("Variable type not supported: " + type); - } + if (end == pos) { + throw new IllegalArgumentException("Empty field name in path: " + path); } - node = addObj(node, paths.get(cellNum), content); + tokens.add(path.substring(pos, end)); + pos = end; } } - return node; + if (tokens.isEmpty() || !(tokens.getFirst() instanceof String)) { + throw new IllegalArgumentException("Path must start with a field name: " + path); + } + return tokens; } + /** + * Sets a value in the nested map/list structure, creating intermediate + * containers as needed. Missing array elements are padded with empty + * maps because Flowable cannot store null list elements as variables. + */ @SuppressWarnings("unchecked") - protected Object addObj(Object parent, String path, Object value) { - if (parent == null) { - parent = new LinkedHashMap(); - } - - int delimiterPosition = indexOfFirstDelimiter(path, ".*"); - if (delimiterPosition <= 0) { - if (parent instanceof LinkedHashMap) { - // Key of a value in an ObjectNode - ((Map) parent).put(path, value); - } else if (parent instanceof ArrayList) { - // Index of a value in an ArrayNode - int index = Integer.parseInt(path); - while (((ArrayList) parent).size() <= index) { - // Create empty entries to parent ArrayNode - ((ArrayList) parent).add(new LinkedHashMap ()); + protected void setValue(Map rowMap, List tokens, Object value) { + Object current = rowMap; + for (int i = 0; i < tokens.size(); i++) { + Object token = tokens.get(i); + boolean last = i == tokens.size() - 1; + if (token instanceof String key) { + if (!(current instanceof Map)) { + throw new IllegalArgumentException("Conflicting paths: '" + key + "' in " + tokens + " addresses an array as an object"); + } + Map map = (Map) current; + if (last) { + map.put(key, value); + } else { + Object child = map.get(key); + if (child == null) { + child = newContainer(tokens.get(i + 1)); + map.put(key, child); + } + current = child; } - ((ArrayList) parent).set(index, value); } else { - throw new RuntimeException("Parent type not supported: " + parent.getClass().getName()); + int index = (Integer) token; + if (!(current instanceof List)) { + throw new IllegalArgumentException("Conflicting paths: index " + index + " in " + tokens + " addresses an object as an array"); + } + List list = (List) current; + while (list.size() <= index) { + list.add(new LinkedHashMap()); + } + if (last) { + list.set(index, value); + } else { + current = list.get(index); + } } - return parent; } + } - String key = path.substring(0, delimiterPosition); - String newPath = path.substring(delimiterPosition + 1); - String delimiter = path.substring(delimiterPosition, delimiterPosition + 1); - Object child = null; - if (delimiter.equals(".")) { - child = new LinkedHashMap(); - } else if (delimiter.equals("*")) { - child = new ArrayList<>(); - } - - Object node; - if (parent instanceof LinkedHashMap) { - // Key of a JsonNode in an ObjectNode - node = ((Map) parent).get(key); - if (node != null) { - // Take the existing ObjectNode as child - child = node; - } - ((Map) parent).put(key, addObj(child, newPath, value)); - } else if (parent instanceof ArrayList) { - // Index of a JsonNode in an ArrayNode - int index = Integer.parseInt(key); - if (((ArrayList) parent).size() > index) { - node = ((ArrayList) parent).get(index); - } else { - node = null; - } - if (node != null) { - // Take the existing ArrayNode as child - child = node; - } - while (((ArrayList) parent).size() <= index) { - // Create empty entries to parent ArrayNode - ((ArrayList) parent).add(new LinkedHashMap ()); - } - ((ArrayList) parent).set(index, addObj(child, newPath, value)); - } else { - throw new RuntimeException("Parent type not supported: " + parent.getClass().getName()); - } - return parent; + protected Object newContainer(Object nextToken) { + return nextToken instanceof Integer ? new ArrayList<>() : new LinkedHashMap(); } } diff --git a/customer-work/src/test/java/com/customer/work/model/FlowableExcelParser.java b/customer-work/src/test/java/com/customer/work/model/FlowableExcelParser.java index afa1629..7028f3c 100644 --- a/customer-work/src/test/java/com/customer/work/model/FlowableExcelParser.java +++ b/customer-work/src/test/java/com/customer/work/model/FlowableExcelParser.java @@ -4,46 +4,38 @@ import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Component; -import java.io.FileInputStream; import java.io.IOException; -import java.net.URL; +import java.io.InputStream; +import java.io.UncheckedIOException; import java.util.ArrayList; -import java.util.LinkedHashMap; - +import java.util.List; +/** + * Reads an .xlsx workbook (created with Excel or LibreOffice) from the classpath + * and returns all cells as Strings: book > sheets > rows > cells. + * Restrictions per sheet: + * - The column count is defined by the first row: it starts with the 1st cell + * and ends at the 1st empty cell + * - Parsing stops at the 1st empty row + * - Formulas in cells are evaluated + */ @Component public class FlowableExcelParser { - // excelPath defines the location of the Excel file - // Return all cells in all sheets with the following restrictions: - // - Max column count per sheet is defined in the first row - // - Max column count starts with the 1st cell and ends with the 1st empty cell - // - Parsing the sheet stops after the 1st empty row - // All sheets are added to an ArrayList - // All rows are added to an ArrayList in the sheets ArrayList - // All cell values are added to an ArrayList in the rows ArrayList - // Formulas in cells are evaluated - // All values are returned as String - public ArrayList>> parseExcelBookFromResource(String excelResourcePath) { - try (FileInputStream fileInputStream = new FileInputStream(getResourcePath(excelResourcePath))) { - Workbook workBook = new XSSFWorkbook(fileInputStream); + public List>> parseExcelBookFromResource(String excelResourcePath) { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(excelResourcePath); + if (inputStream == null) { + throw new IllegalArgumentException("Resource not found: " + excelResourcePath); + } + try (inputStream; Workbook workBook = new XSSFWorkbook(inputStream)) { return parseBook(workBook); } catch (IOException e) { - throw new RuntimeException(e); + throw new UncheckedIOException("Cannot read Excel resource: " + excelResourcePath, e); } } - private String getResourcePath(String resourcePath) { - ClassLoader classLoader = FlowableExcelParser.class.getClassLoader(); - URL resourceUrl = classLoader.getResource(resourcePath); - if (resourceUrl == null) { - throw new RuntimeException("Resource not found: " + resourcePath); - } - return resourceUrl.getPath(); - } - - private ArrayList>> parseBook(Workbook workBook ) { - ArrayList>> book = new ArrayList<>(); + protected List>> parseBook(Workbook workBook) { + List>> book = new ArrayList<>(); FormulaEvaluator formulaEvaluator = workBook.getCreationHelper().createFormulaEvaluator(); for (Sheet workSheet : workBook) { book.add(parseSheet(workSheet, formulaEvaluator)); @@ -51,88 +43,64 @@ public class FlowableExcelParser { return book; } - private ArrayList> parseSheet(Sheet workSheet, FormulaEvaluator formulaEvaluator) { - ArrayList> sheet = new ArrayList<>(); - // Count columns of first row + protected List> parseSheet(Sheet workSheet, FormulaEvaluator formulaEvaluator) { + List> sheet = new ArrayList<>(); int maxCols = getMaxCols(workSheet.getRow(0)); + if (maxCols == 0) { + // Sheet without a header row is empty + return sheet; + } DataFormatter dataFormatter = new DataFormatter(); - int firstRow = workSheet.getFirstRowNum(); int lastRow = workSheet.getLastRowNum(); - // For (Row workRow : workSheet) sometimes ignores empty rows, then the first empty row as an exit criterion wouldn't work - for (int rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) { - ArrayList row = parseRow(workSheet.getRow(rowIndex), maxCols, formulaEvaluator, dataFormatter); + // for (Row workRow : workSheet) sometimes skips empty rows, then the first empty row as an exit criterion wouldn't work + for (int rowIndex = 0; rowIndex <= lastRow; rowIndex++) { + List row = parseRow(workSheet.getRow(rowIndex), maxCols, formulaEvaluator, dataFormatter); if (row == null) { // Take all rows until the first empty row break; - } else { - sheet.add(row); } + sheet.add(row); } return sheet; } - private ArrayList parseRow(Row workRow, int maxCols, FormulaEvaluator formulaEvaluator, DataFormatter dataFormatter) { + protected List parseRow(Row workRow, int maxCols, FormulaEvaluator formulaEvaluator, DataFormatter dataFormatter) { if (workRow == null) { // Row is empty return null; } - ArrayList row = new ArrayList<>(); - boolean allCellsNull = true; + List row = new ArrayList<>(); + boolean allCellsEmpty = true; for (int colIndex = 0; colIndex < maxCols; colIndex++) { Cell workCell = workRow.getCell(colIndex); - formulaEvaluator.evaluate(workCell); String content = dataFormatter.formatCellValue(workCell, formulaEvaluator); if (content != null && !content.isEmpty()) { - allCellsNull = false; + allCellsEmpty = false; } row.add(content); } - if (allCellsNull) { + if (allCellsEmpty) { // Row is empty return null; - } else { - return row; } + return row; } - private int getMaxCols(Row workRow) { + protected int getMaxCols(Row workRow) { + if (workRow == null) { + return 0; + } int maxCol = workRow.getLastCellNum(); for (int colIndex = 0; colIndex < maxCol; colIndex++) { Cell workCell = workRow.getCell(colIndex); - if (workCell == null || workCell.getCellType() == CellType.BLANK || workCell.getCellType() == CellType._NONE) { - // Count cells until the first empty cell + // Count cells until the first empty cell; cells holding an empty string + // (formatting leftovers from Excel/LibreOffice) also count as empty + if (workCell == null || workCell.getCellType() == CellType.BLANK || workCell.getCellType() == CellType._NONE + || (workCell.getCellType() == CellType.STRING && workCell.getStringCellValue().isEmpty())) { return colIndex; } } - return maxCol; + return Math.max(maxCol, 0); } - @Deprecated - public ArrayList> parseExcelFromResource(String resourcePath) { - try (FileInputStream fileInputStream = new FileInputStream(getResourcePath(resourcePath))) { - Workbook workBook = new XSSFWorkbook(fileInputStream); - FormulaEvaluator formulaEvaluator = workBook.getCreationHelper().createFormulaEvaluator(); - ArrayList> sheet = parseSheet(workBook.getSheetAt(0), formulaEvaluator); - ArrayList colHeaders = new ArrayList<>(); - ArrayList> rowList = new ArrayList<>(); - for (int rowIndex = 0; rowIndex < sheet.size(); rowIndex++) { - ArrayList row = sheet.get(rowIndex); - if (rowIndex == 0) { - // Header row - for (Object cell : row) { - colHeaders.add(String.valueOf(cell)); - } - continue; - } - LinkedHashMap rowMap = new LinkedHashMap<>(); - for (int colIndex = 0; colIndex < row.size(); colIndex++) { - rowMap.put(colHeaders.get(colIndex), row.get(colIndex)); - } - rowList.add(rowMap); - } - return rowList; - } catch (IOException e) { - throw new RuntimeException(e); - } - } } diff --git a/customer-work/src/test/java/com/customer/work/model/FlowableModelTestUtils.java b/customer-work/src/test/java/com/customer/work/model/FlowableModelTestUtils.java index a2ef4df..8f90ef2 100644 --- a/customer-work/src/test/java/com/customer/work/model/FlowableModelTestUtils.java +++ b/customer-work/src/test/java/com/customer/work/model/FlowableModelTestUtils.java @@ -9,7 +9,6 @@ import com.flowable.core.spring.security.SecurityUtils; import com.flowable.platform.service.task.CompleteFormRepresentation; import com.flowable.platform.service.task.PlatformTaskService; import jakarta.mail.Address; -import org.apache.commons.lang3.tuple.Pair; import org.assertj.core.api.Assertions; import org.flowable.bpmn.model.*; import org.flowable.bpmn.model.Process; @@ -37,8 +36,8 @@ import org.springframework.stereotype.Component; import org.springframework.util.StreamUtils; import org.springframework.web.client.RestClient; -import java.io.File; -import java.io.FileOutputStream; +import java.io.*; +import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.*; import java.util.stream.Collectors; @@ -59,9 +58,13 @@ public class FlowableModelTestUtils { protected final AuditService auditService; - public static String TENANT_ID = null; - public static String ROOT_PROCESS_ID = "ROOT_PROCESS_ID"; - public static String TEST_PROCESS_ID = "TEST_PROCESS_ID"; + public static final String TENANT_ID = null; + public static final String ROOT_PROCESS_ID = "ROOT_PROCESS_ID"; + public static final String TEST_PROCESS_ID = "TEST_PROCESS_ID"; + + public static final String ROOT_PARAM = "__ROOT"; + public static final String IN_PARAM = "__IN"; + public static final String OUT_PARAM = "__OUT"; public FlowableModelTestUtils(ProcessEngine processEngine, @@ -84,7 +87,7 @@ public class FlowableModelTestUtils { this.auditService = auditService; } - public void checkAndAndAssertAuditRecord(Map map, int auditNumber, String hint, String message, String category, String type) { + public void checkAndAssertAuditRecord(Map map, int auditNumber, String hint, String message, String category, String type) { if (map != null) { Object check = map.get("audit"); if (check instanceof Integer && (Integer) check > 0) { @@ -94,10 +97,9 @@ public class FlowableModelTestUtils { } protected void assertAuditRecord(int auditNumber, String hint, String message, String category, String type) { + if (auditNumber <= 0) return; List auditTrail = getAuditTrail(); - int auditTrailSize = auditTrail.size(); - Assertions.assertThat(auditTrailSize).as(hint + ": invalid auditNumber " + auditTrailSize).isGreaterThanOrEqualTo(auditNumber); - if (auditTrailSize == 0) return; + Assertions.assertThat(auditTrail.size()).as("%s: invalid auditNumber %s", hint, auditNumber).isGreaterThanOrEqualTo(auditNumber); AuditInstance auditInstance = auditTrail.get(auditNumber - 1); Assertions.assertThat(auditInstance.getPayload().get("message")).as(hint).isEqualTo(message); @@ -115,33 +117,14 @@ public class FlowableModelTestUtils { } protected void assertEmail(int emailNumber, String hint, String subject, String receivers) { + if (emailNumber <= 0) return; List emails = getMailList(); - int emailListSize = emails.size(); - Assertions.assertThat(emailListSize).as(hint + ": invalid email number " + emailListSize).isGreaterThanOrEqualTo(emailNumber); - if (emailNumber == 0) return; + Assertions.assertThat(emails.size()).as("%s: invalid email number %s", hint, emailNumber).isGreaterThanOrEqualTo(emailNumber); EmailDto email = emails.get(emailNumber - 1); - String emailSubject = email.getSubject(); - Assertions.assertThat(emailSubject).as(hint + ": invalid subject " + emailSubject).endsWith(subject); - - List> receiverCheckList = new ArrayList<>(); - for (String receiver : receivers.split("[,\\s]+")) { - receiverCheckList.add(Pair.of(receiver, false)); - } - List emailReceiverList = email.getReceiverList(); - Assertions.assertThat(emailReceiverList.size()).as(hint + ": invalid number of receivers " + emailReceiverList).isEqualTo(receiverCheckList.size()); - - for (String emailReceiver : emailReceiverList) { - for (int i = 0; i < receiverCheckList.size(); i++) { - Pair checkReceiver = receiverCheckList.get(i); - if (checkReceiver.getLeft().equals(emailReceiver) && !checkReceiver.getRight()) { - receiverCheckList.set(i, Pair.of(emailReceiver, true)); - break; - } else if (i == receiverCheckList.size() - 1) { - Assertions.fail(hint + ": invalid email receiver " + emailReceiverList); - } - } - } + Assertions.assertThat(email.getSubject()).as("%s: invalid subject %s", hint, email.getSubject()).endsWith(subject); + Assertions.assertThat(email.getReceiverList()).as("%s: invalid email receivers", hint) + .containsExactlyInAnyOrder(receivers.split("[,\\s]+")); } public Map getHistoryCasePayload(String caseInstanceId) { @@ -176,13 +159,13 @@ public class FlowableModelTestUtils { public Task getOpenTask(String taskKey) { Task task = taskService.createTaskQuery().taskDefinitionKey(taskKey).singleResult(); - Assertions.assertThat(task).as("No open task with key {} found", taskKey).isNotNull(); + Assertions.assertThat(task).as("No open task with key %s found", taskKey).isNotNull(); return task; } public Task getOpenTaskByName(String taskName) { Task task = taskService.createTaskQuery().taskName(taskName).singleResult(); - Assertions.assertThat(task).as("No open task with name {} found", taskName).isNotNull(); + Assertions.assertThat(task).as("No open task with name %s found", taskName).isNotNull(); return task; } @@ -281,9 +264,8 @@ public class FlowableModelTestUtils { public Stream getObjArgumentsFromExcel(String path) { ArrayList argumentList = new ArrayList<>(); - ArrayList> book = flowableExcelMapper.excelBookResourceToObj(path); - for (ArrayList sheet : book) { - for (Object row : sheet) { + for (List> sheet : flowableExcelMapper.excelBookResourceToObj(path)) { + for (Map row : sheet) { argumentList.add(Arguments.of(path, row)); } } @@ -304,11 +286,11 @@ public class FlowableModelTestUtils { } JsonNode inParam = row.get("in"); if (inParam != null) { - vars.set("__IN", inParam); + vars.set(IN_PARAM, inParam); } JsonNode outParam = row.get("out"); if (outParam != null) { - vars.set("__OUT", outParam); + vars.set(OUT_PARAM, outParam); } JsonNode idParam = row.get("id"); JsonNode test = row.get("test"); @@ -324,6 +306,7 @@ public class FlowableModelTestUtils { logger.info("TestExcelRow file={} id={} rootParam={} inParam={} outParam={} timer={} test={} audit={} email={} ", path, idParam, rootParam, inParam, outParam, timerCount, test, auditRecordCount, emailCount); + Assertions.assertThat(idParam).as("Column 'id' missing in row of %s", path).isNotNull(); ObjectNode processIds = createRootTestProcessInstance(idParam.asText(), jsonUtils.convertJsonNodeToMap(vars)); for (int i = 0; i < timerCount; i++) { executeTimer(processIds.get(TEST_PROCESS_ID).asText()); @@ -338,16 +321,13 @@ public class FlowableModelTestUtils { result.set("root", root); } if (auditRecordCount != null) { -// List auditTrail = getAuditTrail(processIds.get(ROOT_PROCESS_ID).asText()); - List auditTrail = getAuditTrail(); - int auditTrailSize = auditTrail.size(); - Assertions.assertThat(auditTrailSize).as("Invalid audit trail size: {}", auditTrailSize).isEqualTo(auditRecordCount); + List auditTrail = getAuditTrail(); + Assertions.assertThat(auditTrail.size()).as("Invalid audit trail size: %s", auditTrail.size()).isEqualTo(auditRecordCount); result.set("auditTrail", jsonUtils.convertListToJsonNode(Collections.singletonList(auditTrail))); } if (emailCount != null) { List emailList = getMailList(); - int emailListSize = emailList.size(); - Assertions.assertThat(emailListSize).as("Invalid number of emails: {}", emailListSize).isEqualTo(emailCount); + Assertions.assertThat(emailList.size()).as("Invalid number of emails: %s", emailList.size()).isEqualTo(emailCount); result.set("emails", jsonUtils.convertListToJsonNode(Collections.singletonList(emailList))); } return result; @@ -357,17 +337,15 @@ public class FlowableModelTestUtils { Map vars = new LinkedHashMap<>(); Map rootParam = (Map) row.get("root"); if (rootParam != null) { - for (Map.Entry entry : rootParam.entrySet()) { - vars.put(entry.getKey(), entry.getValue()); - } + vars.putAll(rootParam); } Map inParam = (Map) row.get("in"); if (inParam != null) { - vars.put("__IN", inParam); + vars.put(IN_PARAM, inParam); } Map outParam = (Map) row.get("out"); if (outParam != null) { - vars.put("__OUT", outParam); + vars.put(OUT_PARAM, outParam); } String idParam = (String) row.get("id"); Map test =(Map) row.get("test"); @@ -383,6 +361,7 @@ public class FlowableModelTestUtils { logger.info("TestExcelRow file={} id={} rootParam={} inParam={} outParam={} timer={} test={} audit={} email={} ", path, idParam, rootParam, inParam, outParam, timerCount, test, auditRecordCount, emailCount); + Assertions.assertThat(idParam).as("Column 'id' missing in row of %s", path).isNotNull(); ObjectNode processIds = createRootTestProcessInstance(idParam, vars); for (int i = 0; i < timerCount; i++) { executeTimer(processIds.get(TEST_PROCESS_ID).asText()); @@ -397,16 +376,13 @@ public class FlowableModelTestUtils { result.put("root", rootProcessPayload); } if (auditRecordCount != null) { -// List auditTrail = getAuditTrail(processIds.get(ROOT_PROCESS_ID).asText()); List auditTrail = getAuditTrail(); - int auditTrailSize = auditTrail.size(); - Assertions.assertThat(auditTrailSize).as("Invalid audit trail size: {}", auditTrailSize).isEqualTo(auditRecordCount); + Assertions.assertThat(auditTrail.size()).as("Invalid audit trail size: %s", auditTrail.size()).isEqualTo(auditRecordCount); result.put("audit", auditRecordCount); } if (emailCount != null) { List emailList = getMailList(); - int emailListSize = emailList.size(); - Assertions.assertThat(emailListSize).as("Invalid number of emails: {}", emailListSize).isEqualTo(emailCount); + Assertions.assertThat(emailList.size()).as("Invalid number of emails: %s", emailList.size()).isEqualTo(emailCount); result.put("email", emailCount); } return result; @@ -418,11 +394,13 @@ public class FlowableModelTestUtils { .defaultHeaders(h -> h.setBasicAuth(username, password)) .build(); - File file = new File(Paths.get("src/test/resources/test-auto-deploy-apps", appModelKey + ".zip").toString()); + File file = Paths.get("src", "test", "resources", "test-auto-deploy-apps", appModelKey + ".zip").toFile(); restClient.get() .uri("/design-api/workspaces/" + workspaceKey + "/apps/" + appModelKey + "/export?excludeChildReferences=false") .exchange((req, resp) -> { - StreamUtils.copy(resp.getBody(), new FileOutputStream(file, false)); + try (FileOutputStream outputStream = new FileOutputStream(file, false)) { + StreamUtils.copy(resp.getBody(), outputStream); + } return file; }); } @@ -436,12 +414,49 @@ public class FlowableModelTestUtils { } public ObjectNode loadObjectNodeFromResources(String path) { - if (path.isEmpty()) return jsonUtils.getEmptyObjectNode(); - else return jsonUtils.loadObjectNodeFromFile(Paths.get("src","test", "resources", path).toString()); + if (path == null || path.isEmpty()) return jsonUtils.getEmptyObjectNode(); + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(path)) { + Assertions.assertThat(inputStream).as("Resource not found: %s", path).isNotNull(); + JsonNode jsonNode = jsonUtils.convertJsonStringToJsonNode(new String(inputStream.readAllBytes(), StandardCharsets.UTF_8)); + Assertions.assertThat(jsonNode).as("Resource %s is not a JSON object", path).isInstanceOf(ObjectNode.class); + return (ObjectNode) jsonNode; + } catch (IOException e) { + throw new UncheckedIOException("Cannot read resource: " + path, e); + } } - public Map loadMapFromResources(String path) { - return jsonUtils.convertJsonNodeToMap(loadObjectNodeFromResources(path)); + /** + * Loads process start variables from a JSON resource. All parameters must be + * declared explicitly: root process variables inside "__ROOT", call activity + * in/out mappings inside "__IN" and "__OUT". Any other top-level field fails. + */ + public Map loadTestVarsFromResources(String path) { + ObjectNode node = loadObjectNodeFromResources(path); + Map vars = new LinkedHashMap<>(); + for (Iterator> fields = node.fields(); fields.hasNext(); ) { + Map.Entry field = fields.next(); + String key = field.getKey(); + switch (key) { + case ROOT_PARAM: + Assertions.assertThat(field.getValue().isObject()) + .as("%s in %s must be a JSON object", ROOT_PARAM, path).isTrue(); + for (Iterator rootKeys = field.getValue().fieldNames(); rootKeys.hasNext(); ) { + String rootKey = rootKeys.next(); + Assertions.assertThat(rootKey.startsWith("__")) + .as("Root parameter '%s' in %s must not start with '__'", rootKey, path).isFalse(); + } + vars.putAll(jsonUtils.convertJsonNodeToMap(field.getValue())); + break; + case IN_PARAM: + case OUT_PARAM: + vars.put(key, jsonUtils.convertJsonNodeToMap(field.getValue())); + break; + default: + throw new IllegalArgumentException("Implicit parameter '" + key + "' in " + path + + ": parameters must be declared explicitly inside " + ROOT_PARAM + ", " + IN_PARAM + " or " + OUT_PARAM); + } + } + return vars; } public void executeTimer(String processId) { @@ -450,22 +465,6 @@ public class FlowableModelTestUtils { managementService.moveTimerToExecutableJob(timerJobEntity.getId()); managementService.executeJob(timerJobEntity.getId()); } - public void setTestAuthenticatedUser(String userId, String tenantId) { - if (userId == null) { - this.resetTestAuthenticatedUser(); - } else { - List grantedAuthorities = new ArrayList<>(); - if (StringUtils.isNotBlank(tenantId)) { - grantedAuthorities.add(SecurityUtils.createTenantAuthority(tenantId)); - } - SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken(userId, "", grantedAuthorities)); - AuthenticationContext authenticationContext = Authentication.getAuthenticationContext(); - if (!(authenticationContext instanceof SpringSecurityAuthenticationContext)) { - Authentication.setAuthenticatedUserId(userId); - } - } - } - public void setTestAuthenticatedUser(String userId, String tenantId, String... groupKeys) { if (userId == null) { this.resetTestAuthenticatedUser(); @@ -585,21 +584,21 @@ public class FlowableModelTestUtils { CallActivity callActivity = new CallActivity(); callActivity.setId(testKey); callActivity.setCalledElement(testKey); - JsonNode inNode = variables.get("__IN"); + JsonNode inNode = variables.get(IN_PARAM); if (inNode instanceof ObjectNode) { Map inMap = jsonUtils.convertObjectNodeToMap((ObjectNode) inNode); ArrayList inParameters = new ArrayList<>(); for (Map.Entry entry : inMap.entrySet()) { IOParameter ioParameter = new IOParameter(); - ioParameter.setSourceExpression("${__IN." + entry.getKey() + "}"); + ioParameter.setSourceExpression("${" + IN_PARAM + "." + entry.getKey() + "}"); ioParameter.setTarget(entry.getKey()); inParameters.add(ioParameter); } callActivity.setInParameters(inParameters); } - JsonNode outNode = variables.get("__OUT"); + JsonNode outNode = variables.get(OUT_PARAM); if (outNode instanceof ObjectNode) { - Map outMap = (Map) jsonUtils.convertJsonNodeToMap(variables).get("__OUT"); + Map outMap = (Map) jsonUtils.convertJsonNodeToMap(variables).get(OUT_PARAM); ArrayList outParameters = new ArrayList<>(); for (Map.Entry entry : outMap.entrySet()) { IOParameter ioParameter = new IOParameter(); diff --git a/customer-work/src/test/java/com/customer/work/model/TestMailServer.java b/customer-work/src/test/java/com/customer/work/model/TestMailServer.java index 5ec4162..436b32e 100644 --- a/customer-work/src/test/java/com/customer/work/model/TestMailServer.java +++ b/customer-work/src/test/java/com/customer/work/model/TestMailServer.java @@ -24,10 +24,16 @@ public class TestMailServer { } public void stop() { - greenMail.stop(); + if (greenMail != null) { + greenMail.stop(); + greenMail = null; + } } - public MimeMessage[] getMessages() { + public MimeMessage[] getMessages() { + if (greenMail == null) { + throw new IllegalStateException("TestMailServer is not started, is the TestMailServerExtension registered?"); + } return greenMail.getReceivedMessages(); } } diff --git a/customer-work/src/test/java/com/customer/work/model/test/ModelTest.java b/customer-work/src/test/java/com/customer/work/model/test/ModelTest.java index aa29dab..7a389e3 100644 --- a/customer-work/src/test/java/com/customer/work/model/test/ModelTest.java +++ b/customer-work/src/test/java/com/customer/work/model/test/ModelTest.java @@ -14,7 +14,6 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; -import javax.validation.constraints.NotNull; import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -28,7 +27,7 @@ public class ModelTest { @Test @Disabled public void exportApp() { - flowableModelTest.exportApp("http://localhost:8106", "andiWS", "TST_APP", "admin", "test"); + flowableModelTest.exportApp("http://localhost:8106", "andiWS","TST_APP", "admin", "test"); } @Test @@ -53,7 +52,6 @@ public class ModelTest { Assertions.assertThat(rootProcessPayload.get("dataEntry")).isEqualTo("my root text"); } - @NotNull private Stream p001TestData() { return Stream.of( Arguments.of("admin", "model/test/P001/initiator.json") @@ -64,14 +62,20 @@ public class ModelTest { public void p001Test(String initiator, String path) { flowableModelTest.setTestAuthenticatedUser(initiator, null); ObjectNode processIds = flowableModelTest.createRootTestProcessInstance("TST_P001", - flowableModelTest.loadMapFromResources(path)); + flowableModelTest.loadTestVarsFromResources(path)); flowableModelTest.completeOpenTask("TST_P001_T001", flowableModelTest.emptyNode(), "complete"); Map rootProcessPayload = flowableModelTest.getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText()); Assertions.assertThat(rootProcessPayload.get("rootResult")).isEqualTo(null); } - @NotNull + @Test + public void implicitParameterTest() { + Assertions.assertThatThrownBy(() -> flowableModelTest.loadTestVarsFromResources("model/test/implicit.json")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Implicit parameter 'param'"); + } + private Stream p002TestData() { return Stream.of( Arguments.of("model/test/P002/boolean1.json", true, false), @@ -88,7 +92,7 @@ public class ModelTest { @MethodSource("p002TestData") public void p002Test(String path, Object result, Object out) { ObjectNode processIds = flowableModelTest.createRootTestProcessInstance("TST_P002", - flowableModelTest.loadMapFromResources(path)); + flowableModelTest.loadTestVarsFromResources(path)); Map rootProcessPayload = flowableModelTest.getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText()); List mail = flowableModelTest.getMailList(); @@ -104,7 +108,7 @@ public class ModelTest { @ParameterizedTest @MethodSource("p002ExcelTestData") public void p002ExcelTest(String path, JsonNode argument) { - ObjectNode result = flowableModelTest.testJsonExcelRow(path, argument); + flowableModelTest.testJsonExcelRow(path, argument); } private Stream p002ExcelTestData2() { @@ -116,7 +120,7 @@ public class ModelTest { Map result = flowableModelTest.testObjExcelRow(path, argument); flowableModelTest.checkAndAssertEmail(result, 1, "Email", "Test", "test@flowable.com"); - flowableModelTest.checkAndAndAssertAuditRecord(result, 1, + flowableModelTest.checkAndAssertAuditRecord(result, 1, "Audit record", "TST_P002 Audit trail entry", "system", null); } } diff --git a/customer-work/src/test/resources/model/test/P002/boolean1.json b/customer-work/src/test/resources/model/test/P002/boolean1.json index 6f84512..5dfcae5 100644 --- a/customer-work/src/test/resources/model/test/P002/boolean1.json +++ b/customer-work/src/test/resources/model/test/P002/boolean1.json @@ -1,9 +1,11 @@ { - "param": true, + "__ROOT": { + "param": true + }, "__IN": { "param": false }, "__OUT": { "result": "out" } -} \ No newline at end of file +} diff --git a/customer-work/src/test/resources/model/test/P002/boolean2.json b/customer-work/src/test/resources/model/test/P002/boolean2.json index 3ebab25..b53f990 100644 --- a/customer-work/src/test/resources/model/test/P002/boolean2.json +++ b/customer-work/src/test/resources/model/test/P002/boolean2.json @@ -1,9 +1,11 @@ { - "param": false, + "__ROOT": { + "param": false + }, "__IN": { "param": true }, "__OUT": { "result": "out" } -} \ No newline at end of file +} diff --git a/customer-work/src/test/resources/model/test/P002/date.json b/customer-work/src/test/resources/model/test/P002/date.json index 2829b47..892b2ff 100644 --- a/customer-work/src/test/resources/model/test/P002/date.json +++ b/customer-work/src/test/resources/model/test/P002/date.json @@ -1,9 +1,11 @@ { - "param": "2025-11-12", + "__ROOT": { + "param": "2025-11-12" + }, "__IN": { "param": "2025-11-11" }, "__OUT": { "result": "out" } -} \ No newline at end of file +} diff --git a/customer-work/src/test/resources/model/test/P002/double.json b/customer-work/src/test/resources/model/test/P002/double.json index 38cd1a2..9acb6f2 100644 --- a/customer-work/src/test/resources/model/test/P002/double.json +++ b/customer-work/src/test/resources/model/test/P002/double.json @@ -1,9 +1,11 @@ { - "param": 123.456, + "__ROOT": { + "param": 123.456 + }, "__IN": { "param": 456.789 }, "__OUT": { "result": "out" } -} \ No newline at end of file +} diff --git a/customer-work/src/test/resources/model/test/P002/int.json b/customer-work/src/test/resources/model/test/P002/int.json index cd503ec..b49c140 100644 --- a/customer-work/src/test/resources/model/test/P002/int.json +++ b/customer-work/src/test/resources/model/test/P002/int.json @@ -1,9 +1,11 @@ { - "param": 123, + "__ROOT": { + "param": 123 + }, "__IN": { "param": 456 }, "__OUT": { "result": "out" } -} \ No newline at end of file +} diff --git a/customer-work/src/test/resources/model/test/P002/p002Test.xlsx b/customer-work/src/test/resources/model/test/P002/p002Test.xlsx index b80f353b4fe2ddbdf6cc61c745319f777c74827e..f1cd6b2a3b473281199e34259d3243268ff53038 100644 GIT binary patch delta 1808 zcmZqJ$asAnqeOr=Gm8iV0|N)cuC*pHoaa5=Wf&P45|}qi=rf8TOFe2*jNk=IwXto^ zWki!}&_1cUf{B6Q9oyzZO#7MGAo?adgotfk%PPiPk8DoA*tBIuK((iZ85lqoSLEmy zXCxM-rUaK1W#*+9>s93DOr7qZf6GDS$osz{)82orUmwfT=i$6$l}Q(q%&JvReD|7E ztEX385B&Z1aRBR%ug@1J-kZJMcyrwQFVFmMTj*~x-Wq0X{)OrK#H{73qu!>!t52)_ zZ&)sy>>WC_H>~K{zp2OeYTEhqZaf#T>dYD2i0U~%_T7|CiI(!0Un=$Q=hEHVwj560 z{{8I6$*HM)LASPjT&y5a7k%wiY1yvuvS(cuWpm@M?yvs&^S*wf#+{_BS4mZiX3j|7 zDSe~e&aF)UlCIU-waa&{%XdDW9*Q1PG@fb3H`H0WXH*M%`y45AJklg;Sg!mm?}$wI3~s?< z#U#BWXIf|M6fhP~dggQ{reh|zVXXO|e<8<`~TlUSm9U=DFi3`?Phr_a)SEJZlFMYMT<2xR=dIIK^3Zh4Wd~ zvA$iWYcD0&#fTcD-&?<6!+*(hyW;PjQ#e?9l%wx+`+td9-h#=eBHs7Z{W}=W#glRD zTg0&leaByde`KNsH+6ltHwbJqmixB5x!#~Z* z4-F+ZtH@TdqsXUi3@Y?w17;FMu>4(>05+JJ7eXOs=4kt~q3Ah~Hd*lkP*0lr=7)wK zQ01;KtDBBwo`7Wls(hNm<&}0I`;@@;&9`?&mA}V7b+IW>eiK-JvV1t}W(_xUxSRCC z#Wr{QbfFdkY{|wp62L-WALC|gKLbWl3*)w3j$9uIx;eefJ+DTVu^u)8)y&=GyrulFvJ(7 z<`nDefye-Fn8wMwgXLHn7+o1B7c$DYBg7#FA_^=927Z`nK)Qjk3s^J(i=^`WqU@yn z{A^HZgwPBu(d3vL7~>}=hRD^!%1rbE4Q5;e<3tYZ#T&vvh-(ljje&s|rXB1WQ=o4_ z1!PHOPAa%$L};D-F9hVlD}0j`LX}itjzJ_s1_lWfqq<~(3PmSZhw=h_IJqknq}f;n zB*t%y=F1CU$xT{d4eViH4Ps#+4F-l_F<*#S3|P#}0xTwA$*O~J9mHdZB#7>st#)8d z^X*wD-wWdax#$JhFilsmxO_O+QoV4HrBl2o2ZT$5)Z~KsR=$(_!=*s{)nI;u-{iC5 nvLOB&FyF%;n8cLfL4rtH3=F_J2M!t-^8+S_MyRs+2Z6)@`At@~ delta 2162 zcmcb3w|;Zs}AD#KjQ&hg>6%0ot={l>hw&1GN^ z0Xn@RN542Du_!eqxTGjEFTGf=A~$F5bpQNY4kAb1{}q|mKk?Q7xZNiNCboHmdWxtP zyx_E~;GXs-_EoI=+wJQYY8AXbZ!7g|Z>6>I>#t(l?Qb8eoL)b7<+q59XUp_&U#j?}aP`Ca z^1EA?%F6AYo6k2_M|<=1OWS(oSsOpx-W0ZO^VPkZg2Rt&eEntBzl*2O*Vl16KRm}3 ze7;Ng>=Bt$Zx7ggG2B!={oR`S%eSglY1de5|51Jyf5Z9aRNbcQSu8)U>^3h7-ClIW z#QA8_gawkJhVNJ3TEulXI=|wDG~0S@mdzEtVY336=X^MPL4S`+(oZj)+pFKU-BErq z_gb%x=%Kp6uy+fJ5<3!BU%&dyl0)g*nT8SP66Oo&B)xOhF%-PbD##sng%8b2<~#F_YV{STRlS$eGp|I}^U`%GYwwe7(o>dBvfVS%>be z`P#9);Ox2X>0g-`<)57nZ{vCB=NY8-dhfgYQuQaJHmyxkcz9FuEooeDvp9nqn zF#Xdv^L!sP=p5D*wn}wc9+9!C_*9AC#&aQQR`Qd7?%SgCqv@yO40#Z7qN!77Mp2Wd z`i(&K8LK6&U+I1K?EhI9dSK4aYSz}j=L@d=eO1QBxlY@yV2=Ht;}>GOB!Z7srGNR% zzb`1kA;Nguaie2ZoR`ktSaGkXTj))FTmLyhl^yGT%opfgxKqS%narVxPes$F{L#|f zXIS=4I>4Kqg9QI=XwyMKGEeJK-n4fL|cF*-?b<>G)&*TlEteaokIe`7(aCxO2 z$W2PP?bz(-8VXW-kALc7Q=r;S_|&fPo&YKh&;3{2Rt_xTUIOD5XK5JZYrv?=&cT*! zY$E|wyAOzQsGU6BZ!^%;hu0VyrUOf_k3ft=CBx3ZfcH zhB8i0WR#hFA&7$sXxObFIbhmdHTidt6o@Mx4C3}rwg{F}hSe#^nE_NtBf!PUmBI2L zZL@;q>=_v9xFIEYNo7tdxadZA6mrs+Ko|pL>GDs$XeT}-2eap diff --git a/customer-work/src/test/resources/model/test/P002/string1.json b/customer-work/src/test/resources/model/test/P002/string1.json index 862081d..af1e0aa 100644 --- a/customer-work/src/test/resources/model/test/P002/string1.json +++ b/customer-work/src/test/resources/model/test/P002/string1.json @@ -1,9 +1,11 @@ { - "param": "hello root", + "__ROOT": { + "param": "hello root" + }, "__IN": { "param": "hello" }, "__OUT": { "result": "out" } -} \ No newline at end of file +} diff --git a/customer-work/src/test/resources/model/test/P002/string2.json b/customer-work/src/test/resources/model/test/P002/string2.json index d0b0429..198f45c 100644 --- a/customer-work/src/test/resources/model/test/P002/string2.json +++ b/customer-work/src/test/resources/model/test/P002/string2.json @@ -1,9 +1,11 @@ { - "param": "123", + "__ROOT": { + "param": "123" + }, "__IN": { "param": "456" }, "__OUT": { "result": "out" } -} \ No newline at end of file +} diff --git a/customer-work/src/test/resources/model/test/P002/string3.json b/customer-work/src/test/resources/model/test/P002/string3.json index 91626b3..b870dd1 100644 --- a/customer-work/src/test/resources/model/test/P002/string3.json +++ b/customer-work/src/test/resources/model/test/P002/string3.json @@ -1,9 +1,11 @@ { - "param": "123.456,", + "__ROOT": { + "param": "123.456," + }, "__IN": { "param": "456.789" }, "__OUT": { "result": "out" } -} \ No newline at end of file +} diff --git a/customer-work/src/test/resources/model/test/TrackVars/trackVars.xlsx b/customer-work/src/test/resources/model/test/TrackVars/trackVars.xlsx index f55f2264d5c69a2aee27b300ce29fd0dc2a4f4c5..6b1fbc85a51bb36ba5a20a708136cabead45bc39 100644 GIT binary patch delta 6722 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 EKhA6n*8l(j diff --git a/customer-work/src/test/resources/model/test/implicit.json b/customer-work/src/test/resources/model/test/implicit.json new file mode 100644 index 0000000..6fa2f64 --- /dev/null +++ b/customer-work/src/test/resources/model/test/implicit.json @@ -0,0 +1,6 @@ +{ + "param": true, + "__IN": { + "param": false + } +}