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 b80f353..f1cd6b2 100644 Binary files a/customer-work/src/test/resources/model/test/P002/p002Test.xlsx and b/customer-work/src/test/resources/model/test/P002/p002Test.xlsx differ 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 f55f226..6b1fbc8 100644 Binary files a/customer-work/src/test/resources/model/test/TrackVars/trackVars.xlsx and b/customer-work/src/test/resources/model/test/TrackVars/trackVars.xlsx differ 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 + } +}