Gitlab migration

This commit is contained in:
Andreas Isler
2026-06-23 08:04:38 +02:00
commit 82717986a7
82 changed files with 4872 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>flowable-local-work</artifactId>
<packaging>jar</packaging>
<name>flowable-local-work</name>
<description>flowable local Flowable Work</description>
<parent>
<groupId>com.flowable.local</groupId>
<artifactId>14</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
<!-- flowable local -->
<!-- =============== -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${org.postgresql.version}</version>
</dependency>
<!-- Flowable Frontend -->
<!-- ================= -->
<dependency>
<groupId>com.flowable.work</groupId>
<artifactId>flowable-work-frontend</artifactId>
</dependency>
<!-- Flowable Platform -->
<!-- ================= -->
<dependency>
<groupId>com.flowable.platform</groupId>
<artifactId>flowable-spring-boot-starter-platform-rest</artifactId>
</dependency>
<dependency>
<groupId>com.flowable.platform</groupId>
<artifactId>flowable-tenant-setup</artifactId>
</dependency>
<dependency>
<groupId>com.flowable.platform</groupId>
<artifactId>flowable-platform-default-models</artifactId>
</dependency>
<dependency>
<groupId>com.flowable.platform</groupId>
<artifactId>flowable-platform-default-idm-models</artifactId>
</dependency>
<!-- Flowable Inspect -->
<!-- ================ -->
<dependency>
<groupId>com.flowable.inspect</groupId>
<artifactId>flowable-spring-boot-starter-inspect-rest</artifactId>
</dependency>
<!-- Flowable Actuators -->
<!-- ================== -->
<dependency>
<groupId>com.flowable.platform</groupId>
<artifactId>flowable-spring-boot-starter-platform-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-elastic</artifactId>
</dependency>
<!-- Spring Boot -->
<!-- =========== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<!-- ======= -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${com.h2database.version}</version>
</dependency>
<dependency>
<groupId>com.github.wnameless.json</groupId>
<artifactId>json-flattener</artifactId>
<version>0.16.6</version>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>2.1.8</version>
</dependency>
<dependency>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<version>2.1.4</version>
</dependency>
<!-- AuditTrailController -->
<!-- ======= -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package com.flowable.local.work;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
@SpringBootApplication(exclude = {FreeMarkerAutoConfiguration.class})
public class FlowableLocalWorkApplication {
public static void main(String[] args) {
SpringApplication.run(FlowableLocalWorkApplication.class, args);
}
}
@@ -0,0 +1,39 @@
package com.flowable.local.work.configuration;
import com.flowable.actuate.autoconfigure.security.servlet.ActuatorRequestMatcher;
import com.flowable.platform.common.security.SecurityConstants;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.info.InfoEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityActuatorConfiguration {
@Bean
@Order(6)
public SecurityFilterChain basicActuatorSecurity(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf()
.disable();
http
.requestMatcher(new ActuatorRequestMatcher())
.authorizeRequests()
.requestMatchers(EndpointRequest.to(InfoEndpoint.class, HealthEndpoint.class)).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasAuthority(SecurityConstants.ACCESS_ACTUATORS)
.anyRequest().denyAll()
.and().httpBasic();
return http.build();
}
}
@@ -0,0 +1,79 @@
package com.flowable.local.work.configuration;
import com.flowable.autoconfigure.frontend.FrontendProperties;
import com.flowable.autoconfigure.security.FlowableHttpSecurityCustomizer;
import com.flowable.core.spring.security.web.authentication.AjaxAuthenticationFailureHandler;
import com.flowable.core.spring.security.web.authentication.AjaxAuthenticationSuccessHandler;
import com.flowable.platform.common.security.SecurityConstants;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import java.util.stream.Collectors;
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class SecurityConfiguration {
@Autowired
protected ObjectProvider<FrontendProperties> frontendPropertiesProvider;
@Bean
@Order(10)
public SecurityFilterChain basicDefaultSecurity(HttpSecurity http, ObjectProvider<FlowableHttpSecurityCustomizer> httpSecurityCustomizers) throws Exception {
for (FlowableHttpSecurityCustomizer customizer : httpSecurityCustomizers.orderedStream()
.collect(Collectors.toList())) {
customizer.customize(http);
}
http
.logout()
.logoutUrl("/auth/logout");
FrontendProperties frontendProperties = frontendPropertiesProvider.getIfAvailable();
if (frontendProperties != null && frontendProperties.getFeatures().containsKey("formBasedLogout")
&& frontendProperties.getFeatures().get("formBasedLogout")) {
http
.logout()
.logoutSuccessUrl("/");
} else {
http
.logout()
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
}
http
.exceptionHandling()
.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), AnyRequestMatcher.INSTANCE)
.and()
.formLogin()
.loginProcessingUrl("/auth/login")
.successHandler(new AjaxAuthenticationSuccessHandler())
.failureHandler(new AjaxAuthenticationFailureHandler())
.and()
.authorizeRequests()
.antMatchers("/analytics-api/**").hasAuthority(SecurityConstants.ACCESS_REPORTS_METRICS)
.antMatchers("/work-object-api/**").hasAuthority(SecurityConstants.ACCESS_WORKOBJECT_API)
// allow context root for all (it triggers the loading of the initial page)
.antMatchers("/").permitAll()
.antMatchers(
"/**/*.svg", "/**/*.ico", "/**/*.png", "/**/*.woff2", "/**/*.css",
"/**/*.woff", "/**/*.html", "/**/*.js",
"/**/flowable-frontend-configuration",
"/**/index.html").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
return http.build();
}
}
@@ -0,0 +1,20 @@
package com.flowable.local.work.configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.flowable.local.work.service.StartProcessBot;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RuntimeService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ServiceConfiguration {
@Bean
public StartProcessBot activateTaskBot(RuntimeService runtimeService,
ObjectMapper objectMapper,
HistoryService historyService) {
return new StartProcessBot(runtimeService, objectMapper, historyService);
}
}
@@ -0,0 +1,65 @@
package com.flowable.local.work.configuration;
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 {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.setOrder(10)
.addResourceHandler("/ext/*.js", "/*/ext/*.js")
.addResourceLocations("classpath:/static/ext/", "classpath:/public/ext/")
.setCacheControl(CacheControl.noCache());
registry.setOrder(20)
.addResourceHandler("/ext/*.css", "/*/ext/*.css")
.addResourceLocations("classpath:/static/ext/", "classpath:/public/ext/")
.setCacheControl(CacheControl.noCache());
registry.setOrder(30)
.addResourceHandler("/*.js")
.addResourceLocations("classpath:/public/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
registry.setOrder(40)
.addResourceHandler("/*.css")
.addResourceLocations("classpath:/public/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
registry.setOrder(50)
.addResourceHandler("/*.woff")
.addResourceLocations("classpath:/public/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
registry.setOrder(60)
.addResourceHandler("/*.woff2")
.addResourceLocations("classpath:/public/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
registry.setOrder(70)
.addResourceHandler("/*.svg")
.addResourceLocations("classpath:/public/", "classpath:/public/twemoji/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
registry.setOrder(80)
.addResourceHandler("/*.map")
.addResourceLocations("classpath:/public/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
registry.setOrder(90)
.addResourceHandler("/*.png")
.addResourceLocations("classpath:/public/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
registry.setOrder(100)
.addResourceHandler("/*.ico")
.addResourceLocations("classpath:/public/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
}
}
@@ -0,0 +1,95 @@
package com.flowable.local.work.events;
import com.flowable.audit.api.AuditService;
import com.flowable.dataobject.api.event.FlowableDataObjectEventType;
import com.flowable.dataobject.api.runtime.DataObjectInstanceVariableContainer;
import com.flowable.dataobject.api.runtime.DataObjectRuntimeService;
import com.flowable.dataobject.engine.delegate.event.impl.FlowableDataObjectInstanceDeletedEventImpl;
import com.flowable.dataobject.engine.delegate.event.impl.FlowableDataObjectInstanceUpdatedEventImpl;
import org.flowable.common.engine.api.delegate.event.FlowableEvent;
import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
import org.flowable.common.engine.api.delegate.event.FlowableEventType;
import org.flowable.common.engine.impl.cfg.TransactionState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
public class CustomDataObjectEventHandler implements FlowableEventListener {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomDataObjectEventHandler.class);
protected final AuditService auditService;
protected final DataObjectRuntimeService dataObjectRuntimeService;
public CustomDataObjectEventHandler(AuditService auditService, DataObjectRuntimeService dataObjectRuntimeService) {
this.auditService = auditService;
this.dataObjectRuntimeService = dataObjectRuntimeService;
}
@Override
public void onEvent(FlowableEvent flowableEvent) {
DataObjectInstanceVariableContainer dataObjectInstance;
String subType;
if (flowableEvent instanceof FlowableDataObjectInstanceUpdatedEventImpl) {
dataObjectInstance = ((FlowableDataObjectInstanceUpdatedEventImpl) flowableEvent).getDataObjectInstance();
subType = "updated";
} else if (flowableEvent instanceof FlowableDataObjectInstanceDeletedEventImpl) {
dataObjectInstance = ((FlowableDataObjectInstanceDeletedEventImpl) flowableEvent).getDataObjectInstance();
subType = "deleted";
} else {
LOGGER.warn("Unhandled FlowableDataObjectEvent type: {}", flowableEvent.getClass().getName());
return;
}
HashMap<String, Object> data = (HashMap<String, Object>) dataObjectInstance.getData();
if (data == null) {
LOGGER.warn("FlowableDataObjectEvent: No data");
return;
}
// If an instance of TST_DO001 is deactivated, delete all references in other data objects.
String scopeDefinitionId = dataObjectInstance.getDefinitionKey();
if (scopeDefinitionId == null || !scopeDefinitionId.equals("TST_DO001")) return;
Object active = data.get("active");
if (!(active instanceof Boolean)) {
LOGGER.warn("FlowableDataObjectEvent: 'active' is not boolean");
return;
}
if (subType.equals("updated") && !((Boolean) active) || subType.equals("deleted")) {
String key = (String) data.get("key");
LOGGER.warn("delete all references of {}", key);
dataObjectRuntimeService.createDataObjectModificationBuilder()
.tenantId(dataObjectInstance.getTenantId())
.definitionKey("TST_DO002")
.operation("updateTeam")
.value("teamKey", key)
.value("team", null)
.modify();
}
}
@Override
public boolean isFailOnException() {
return true;
}
@Override
public boolean isFireOnTransactionLifecycleEvent() {
return false;
}
@Override
public String getOnTransaction() {
return TransactionState.COMMITTING.name();
}
@Override
public Collection<? extends FlowableEventType> getTypes() {
List<FlowableEventType> eventTypes = new ArrayList<>();
eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_UPDATED);
eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_DELETED);
return eventTypes;
}
}
@@ -0,0 +1,156 @@
package com.flowable.local.work.events;
import com.flowable.audit.api.AuditService;
import com.flowable.core.common.api.security.SecurityScope;
import com.flowable.core.spring.security.SecurityUtils;
import com.flowable.dataobject.api.event.FlowableDataObjectEventType;
import com.flowable.dataobject.api.runtime.DataObjectInstanceVariableContainer;
import com.flowable.dataobject.engine.delegate.event.impl.*;
import org.flowable.common.engine.api.delegate.event.FlowableEvent;
import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
import org.flowable.common.engine.api.delegate.event.FlowableEventType;
import org.flowable.common.engine.impl.cfg.TransactionState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.*;
/*
Writes audit trail entries on changes of data objects.
These entries must be retrieved by a specific REST endpoint (implemented in AuditTrailController),
because the flowable audit-trail API supports only audit trail entries with scopeType cmmn and bpmn.
The class writes the following data into the table FLW_AUDIT_INSTANCE:
- ID_ autogenerated ID
- CREATION_TIME_ now
- CREATOR_ID_ current user ID
- TYPE_ "user" or "system"
- SUB_TYPE_ "created" / "updated" / "deleted"
- SCOPE_DEFINITION_ID_ data object model key
- SCOPE_ID_ data object instance ID
- SCOPE_TYPE_ "dataObject"
- PAYLOAD_ {
<data object column name 1>: <content of data object field in column 1>
<data object column name 2>: <content of data object field in column 1>
etc.
}
- TENANT_ID_ tenant ID
- SUB_SCOPE_ID_ content of data object field in column "key", if column "key" exists
-> this mapping makes "key" filterable in a data table
*/
@Component
public class GenericDataObjectAuditTrailEventHandler implements FlowableEventListener {
private static final Logger LOGGER = LoggerFactory.getLogger(GenericDataObjectAuditTrailEventHandler.class);
protected final AuditService auditService;
public GenericDataObjectAuditTrailEventHandler(AuditService auditService) {
this.auditService = auditService;
}
@Override
public void onEvent(FlowableEvent flowableEvent) {
DataObjectInstanceVariableContainer dataObjectInstance = null;
String scopeDefinitionId = null;
String operation = null;
Map<String, Object> modificationData = Map.of();
String subType;
if (flowableEvent instanceof FlowableDataObjectInstanceCreatedEventImpl) {
dataObjectInstance = ((FlowableDataObjectInstanceCreatedEventImpl) flowableEvent).getDataObjectInstance();
subType = "created";
} else if (flowableEvent instanceof FlowableDataObjectInstanceUpdatedEventImpl) {
dataObjectInstance = ((FlowableDataObjectInstanceUpdatedEventImpl) flowableEvent).getDataObjectInstance();
subType = "updated";
} else if (flowableEvent instanceof FlowableDataObjectInstanceDeletedEventImpl) {
dataObjectInstance = ((FlowableDataObjectInstanceDeletedEventImpl) flowableEvent).getDataObjectInstance();
subType = "deleted";
} else if (flowableEvent instanceof FlowableDataObjectInstancesBulkUpdatedEventImpl) {
scopeDefinitionId = ((FlowableDataObjectInstancesBulkUpdatedEventImpl) flowableEvent).getDataObjectDefinition().getKey();
operation = ((FlowableDataObjectInstancesBulkUpdatedEventImpl) flowableEvent).getOperation();
modificationData = ((FlowableDataObjectInstancesBulkUpdatedEventImpl) flowableEvent).getModificationData();
subType = "updatedBulk";
} else if (flowableEvent instanceof FlowableDataObjectInstancesBulkDeletedEventImpl) {
scopeDefinitionId = ((FlowableDataObjectInstancesBulkDeletedEventImpl) flowableEvent).getDataObjectDefinition().getKey();
operation = ((FlowableDataObjectInstancesBulkDeletedEventImpl) flowableEvent).getOperation();
subType = "deletedBulk";
} else {
LOGGER.warn("Unhandled FlowableDataObjectEvent type: {}", flowableEvent.getClass().getName());
return;
}
Map<String, Object> payload = new HashMap<>();
String scopeId = null;
String subScopeId = null;
SecurityScope currentUserSecurityScope = SecurityUtils.getCurrentUserSecurityScopeSafe();
if (subType.equals("created") || subType.equals("updated") || subType.equals("deleted")) {
HashMap<String, Object> data = (HashMap<String, Object>) dataObjectInstance.getData();
if (data == null) {
LOGGER.warn("FlowableDataObjectEvent: No data");
return;
}
scopeDefinitionId = dataObjectInstance.getDefinitionKey();
for (Map.Entry<String, Object> entry : data.entrySet()) {
if (entry.getKey().equals("id")) {
scopeId = (String) entry.getValue();
} else if (entry.getKey().equals("key")) {
// If there is a "key" in the data object, expose it as subScopeId for making it filterable.
subScopeId = (String) entry.getValue();
} else {
payload.put(entry.getKey(), entry.getValue());
}
}
} else {
payload.put("operation", operation);
payload.putAll(modificationData);
}
if (currentUserSecurityScope == null) {
auditService.createAuditInstanceBuilder()
.type("system")
.subType(subType)
.scopeType("dataObject")
.scopeDefinitionId(scopeDefinitionId)
.scopeId(scopeId)
.subScopeId(subScopeId)
.payload(payload)
.create();
} else {
auditService.createAuditInstanceBuilder()
.type("user")
.creatorId(currentUserSecurityScope.getUserId())
.tenantId(currentUserSecurityScope.getTenantId())
.subType(subType)
.scopeType("dataObject")
.scopeDefinitionId(scopeDefinitionId)
.scopeId(scopeId)
.subScopeId(subScopeId)
.payload(payload)
.create();
}
}
@Override
public boolean isFailOnException() {
return true;
}
@Override
public boolean isFireOnTransactionLifecycleEvent() {
return false;
}
@Override
public String getOnTransaction() {
return TransactionState.COMMITTING.name();
}
@Override
public Collection<? extends FlowableEventType> getTypes() {
List<FlowableEventType> eventTypes = new ArrayList<>();
eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_CREATED);
eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_UPDATED);
eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_DELETED);
return eventTypes;
}
}
@@ -0,0 +1,66 @@
package com.flowable.local.work.rest;
import com.flowable.autoconfigure.platform.PropertyConfigurationService;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/*
http://localhost:8090/custom-api/application-property/keyDoesNotExist -> null
http://localhost:8090/custom-api/application-property/string/keyDoesNotExist -> null
http://localhost:8090/custom-api/application-property/keyDoesNotExist?default=abc -> abc
http://localhost:8090/custom-api/application-property/string/keyDoesNotExist?default=abc -> abc
http://localhost:8090/custom-api/application-property/server.port -> 8090
http://localhost:8090/custom-api/application-property/string/server.port -> 8090
http://localhost:8090/custom-api/application-property/server.port?default=abc -> 8090
http://localhost:8090/custom-api/application-property/string/server.port?default=abc -> 8090
http://localhost:8090/custom-api/application-property/boolean/keyDoesNotExist -> false
http://localhost:8090/custom-api/application-property/boolean/keyDoesNotExist?default=true -> true
http://localhost:8090/custom-api/application-property/boolean/flowable.platform.idm.minimal-setup -> true
http://localhost:8090/custom-api/application-property/boolean/flowable.platform.idm.minimal-setup?default=false -> true
http://localhost:8090/custom-api/application-property/integer/keyDoesNotExist -> 0
http://localhost:8090/custom-api/application-property/integer/keyDoesNotExist?default=123 -> true
http://localhost:8090/custom-api/application-property/integer/server.port -> 8090
http://localhost:8090/custom-api/application-property/integer/server.port?default=123 -> 8090
http://localhost:8090/custom-api/application-property/integer/flowable.platform.idm.minimal-setup -> ConversionFailedException
*/
@RestController
@RequestMapping("/custom-api")
@Validated
public class ApplicationPropertyController {
protected final PropertyConfigurationService propertyConfigurationService;
public ApplicationPropertyController(PropertyConfigurationService propertyConfigurationService) {
this.propertyConfigurationService = propertyConfigurationService;
}
@GetMapping(value = {"/application-property/{key}", "/application-property/string/{key}"})
public ResponseEntity<String> getStringVariable(
@PathVariable String key,
@RequestParam(name = "default", required = false) String defaultValue) {
return ResponseEntity.ok(propertyConfigurationService.getProperty(key, defaultValue));
}
@GetMapping("/application-property/boolean/{key}")
public ResponseEntity<Boolean> getBooleanVariable(
@PathVariable String key,
@RequestParam(name = "default", required = false) Boolean defaultValue) {
if (defaultValue == null) {
defaultValue = false;
}
return ResponseEntity.ok(propertyConfigurationService.getBooleanProperty(key, defaultValue));
}
@GetMapping("/application-property/integer/{key}")
public ResponseEntity<Integer> getIntegerVariable(
@PathVariable String key,
@RequestParam(name = "default", required = false) Integer defaultValue) {
if (defaultValue == null) {
defaultValue = 0;
}
return ResponseEntity.ok(propertyConfigurationService.getIntegerProperty(key, defaultValue));
}
}
@@ -0,0 +1,52 @@
package com.flowable.local.work.rest;
import com.flowable.local.work.service.AuditTrailService;
import org.flowable.common.rest.api.DataResponse;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.HashMap;
/*
Examples:
http://localhost:8090/custom-api/audit-trail?scopeType=dataObject&dataObjectModelKey=TST_DO001
http://localhost:8090/custom-api/audit-trail?scopeType=dataObject&dataObjectId=d05bd02b-3f8a-11f0-9e89-2e48f5a806de
http://localhost:8090/custom-api/audit-trail?scopeType=dataObject&scopeId=d05bd02b-3f8a-11f0-9e89-2e48f5a806de
http://localhost:8090/custom-api/audit-trail?scopeType=cmmn&instanceName=TST-000001
http://localhost:8090/custom-api/audit-trail?scopeType=cmmn&rootId=CAS-9270a424-3fa3-11f0-8c07-2e48f5a806de
http://localhost:8090/custom-api/audit-trail?scopeType=cmmn&scopeId=CAS-9270a424-3fa3-11f0-8c07-2e48f5a806de
*/
@RestController
@RequestMapping("/custom-api")
@Validated
public class AuditTrailController {
protected final AuditTrailService auditTrailService;
public AuditTrailController(AuditTrailService auditTrailService) {
this.auditTrailService = auditTrailService;
}
@GetMapping(value = {"/audit-trail"}, produces = {"application/json"})
public DataResponse<HashMap<String, Object>> getAuditTrail(
@RequestParam(name = "scopeType") @NotNull @NotEmpty String scopeType,
@RequestParam(name = "scopeId", required = false) String scopeId,
@RequestParam(name = "rootId", required = false) String rootId,
@RequestParam(name = "instanceName", required = false) String instanceName,
@RequestParam(name = "dataObjectId", required = false) String dataObjectId,
@RequestParam(name = "dataObjectModelKey", required = false) String dataObjectModelKey,
@RequestParam(name = "start", required = false) Integer start,
@RequestParam(name = "size", required = false) Integer size,
@RequestParam(name = "createdAfter", required = false) String createdAfter,
@RequestParam(name = "createdBefore", required = false) String createdBefore,
@RequestParam(name = "creatorId", required = false) String creatorId,
@RequestParam(name = "subType", required = false) String subType,
@RequestParam(name = "subScopeId", required = false) String subScopeId) {
return this.auditTrailService.getAuditTrail(scopeType, scopeId, rootId, instanceName, dataObjectId, dataObjectModelKey,
start, size, createdAfter, createdBefore, creatorId, subType, subScopeId);
}
}
@@ -0,0 +1,38 @@
package com.flowable.local.work.rest;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.flowable.local.work.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/*
Examples:
http://localhost:8090/custom-api/current-user-groups
http://localhost:8090/custom-api/user-groups/{userId}
*/
@RestController
@RequestMapping("/custom-api")
@Validated
public class UserController {
protected final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping(value = {"/current-user-groups"}, produces = {"application/json"})
public ResponseEntity<ArrayNode> getCurrentUserGroups() {
return ResponseEntity.ok(this.userService.getCurrentUserGroups());
}
@GetMapping(value = {"/user-groups/{userId}"}, produces = {"application/json"})
public ResponseEntity<ArrayNode> getUserGroups(@PathVariable String userId) {
return ResponseEntity.ok(this.userService.getUserGroups(userId));
}
}
@@ -0,0 +1,44 @@
package com.flowable.local.work.rest;
import com.fasterxml.jackson.databind.JsonNode;
import com.flowable.local.work.service.VariableService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.*;
/*
http://localhost:8090/custom-api/variable/CAS-e5d25542-0947-11f0-8b0d-e6be909bfdcd/caseNname
http://localhost:8090/custom-api/variable/CAS-e5d25542-0947-11f0-8b0d-e6be909bfdcd
http://localhost:8090/custom-api/variable/root
http://localhost:8090/platform-api/case-instances/CAS-5dc9c774-42d4-11f0-b269-2e48f5a806de/work-form/variables
http://localhost:8090/custom-api/variable/CAS-5dc9c774-42d4-11f0-b269-2e48f5a806de/name
*/
@RestController
@RequestMapping("/custom-api")
public class VariableController {
protected final VariableService variableService;
public VariableController(VariableService variableService) {
this.variableService = variableService;
}
@GetMapping("/variable/{caseInstanceId}")
public ResponseEntity<JsonNode> getRootVariable(@PathVariable String caseInstanceId) {
return ResponseEntity.ok(variableService.getVariable(caseInstanceId, "root"));
}
@GetMapping("/variable/{caseInstanceId}/{variableName}")
public ResponseEntity<JsonNode> getSpecificVariable(@PathVariable String caseInstanceId, @PathVariable String variableName) {
return ResponseEntity.ok(variableService.getVariable(caseInstanceId, variableName));
}
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handleMissingServletRequestParameterException() {
return "Variable not found";
}
}
@@ -0,0 +1,153 @@
package com.flowable.local.work.service;
import com.flowable.audit.api.AuditService;
import com.flowable.audit.api.runtime.AuditInstance;
import com.flowable.audit.api.runtime.AuditInstanceQuery;
import com.flowable.audit.engine.impl.persistence.entity.AuditInstanceEntity;
import org.flowable.cmmn.api.history.HistoricCaseInstance;
import org.flowable.cmmn.api.runtime.CaseInstance;
import org.flowable.cmmn.engine.CmmnEngine;
import org.flowable.common.rest.api.DataResponse;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.*;
/*
Returns audit trail entries based on:
- scopeType cmmn
- rootId <root.id>
-> audit trail entries for case with specified id
- instanceName <root.name>
-> audit trail entries for case with specified name
- scopeType dataObject
- dataObjectId <data object instance id>
-> audit trail entries for data object with specified instance id, provided by DataObjectAuditTrailEventHandler
- dataObjectModelKey <data object model key>
-> audit trail entries for data object with specified model key, provided by DataObjectAuditTrailEventHandler
*/
@Component
public class AuditTrailService {
protected static final Logger LOGGER = LoggerFactory.getLogger(AuditTrailService.class);
protected final CmmnEngine cmmnEngine;
protected final AuditService auditService;
public AuditTrailService(CmmnEngine cmmnEngine, AuditService auditService) {
this.cmmnEngine = cmmnEngine;
this.auditService = auditService;
}
public DataResponse<HashMap<String, Object>> getAuditTrail(String scopeType, String scopeId, String rootId, String instanceName,
String dataObjectId, String dataObjectModelKey,
Integer start, Integer size,
String createdAfter, String createdBefore,
String creatorId, String subType, String subScopeId) {
String scopeDefinitionId = null;
switch (scopeType) {
case "cmmn":
// Check for rootId or instanceName if scopeId is not provided
if (scopeId == null || scopeId.trim().isEmpty()) {
if (rootId != null && !rootId.trim().isEmpty()) {
scopeId = rootId;
} else if (instanceName != null && !instanceName.trim().isEmpty()) {
CaseInstance caseInstance = this.cmmnEngine.getCmmnRuntimeService().createCaseInstanceQuery().caseInstanceBusinessKey("asdf").singleResult();
// CaseInstance caseInstance = this.cmmnEngine.getCmmnRuntimeService().createCaseInstanceQuery().caseInstanceName(instanceName).singleResult();
if (caseInstance != null) {
scopeId = caseInstance.getId();
} else {
HistoricCaseInstance historicCaseInstance = this.cmmnEngine.getCmmnHistoryService().createHistoricCaseInstanceQuery().caseInstanceName(instanceName).singleResult();
if (historicCaseInstance != null) {
scopeId = historicCaseInstance.getId();
}
}
} else {
LOGGER.warn("Missing parameter rootId or instanceName for scopeType=cmmn");
return createAuditEntryDataResponse(null, 0, 0);
}
}
break;
case "dataObject":
// Check for dataObjectId or dataObjectModelKey if scopeId is not provided
if (scopeId == null || scopeId.trim().isEmpty()) {
if (dataObjectId != null && !dataObjectId.trim().isEmpty()) {
scopeId = dataObjectId;
} else if (dataObjectModelKey != null && !dataObjectModelKey.trim().isEmpty()) {
scopeDefinitionId = dataObjectModelKey;
} else {
LOGGER.warn("Missing parameter dataObjectId or dataObjectModelKey for scopeType=dataObject");
return createAuditEntryDataResponse(null, 0, 0);
}
}
break;
default:
LOGGER.warn("Invalid scopeType: {}", scopeType);
return createAuditEntryDataResponse(null, 0, 0);
}
return queryAuditTrail(scopeType, scopeId, scopeDefinitionId, start, size, createdAfter, createdBefore, creatorId, subType, subScopeId);
}
protected DataResponse<HashMap<String, Object>> queryAuditTrail(String scopeType, String scopeId,
String scopeDefinitionId,
Integer start, Integer size,
String createdAfter, String createdBefore,
String creatorId, String subType, String subScopeId) {
// Create query from parameters
AuditInstanceQuery query = this.auditService.createAuditInstanceQuery().orderByCreationTime().desc();
query.scopeType(scopeType);
query.scopeId(scopeId);
query.scopeDefinitionId(scopeDefinitionId);
if (createdAfter != null && !createdAfter.trim().isEmpty()) {
query.createdAfter(DateTime.parse(createdAfter).toDate());
}
if (createdBefore != null && !createdBefore.trim().isEmpty()) {
query.createdBefore(DateTime.parse(createdBefore).toDate());
}
query.creatorId(creatorId);
query.subType(subType);
query.subScopeId(subScopeId);
// Run paged query
int startIndex = start == null ? 0 : start;
List<AuditInstance> auditInstances;
if (size != null) {
auditInstances = query.listPage(startIndex, size);
} else {
auditInstances = query.list();
}
// Map query result into proper list
List<HashMap<String, Object>> entries = new ArrayList<>();
for (AuditInstance auditInstance : auditInstances) {
entries.add(convertAuditInstance(auditInstance));
}
return createAuditEntryDataResponse(entries, query.count(), startIndex);
}
protected DataResponse<HashMap<String, Object>> createAuditEntryDataResponse(List<HashMap<String, Object>> entries, long totalCount, int startIndex) {
DataResponse<HashMap<String, Object>> auditTrail = new DataResponse<>();
if (entries != null && !entries.isEmpty()) {
auditTrail.setData(entries);
auditTrail.setTotal(totalCount);
auditTrail.setStart(startIndex);
auditTrail.setSize(entries.size());
}
auditTrail.setSort("creationTime");
auditTrail.setOrder("desc");
return auditTrail;
}
protected HashMap<String, Object> convertAuditInstance(AuditInstance auditInstance) {
HashMap<String, Object> auditEntry = new HashMap<>();
AuditInstanceEntity auditInst = (AuditInstanceEntity) auditInstance;
auditEntry.put("id", auditInst.getId());
auditEntry.putAll((Map<String, String>) auditInst.getPersistentState());
HashMap<String, Object> auditInstPayload = (HashMap<String, Object>) auditInst.getPayload();
if (auditInstPayload != null) {
auditEntry.put("payload", new HashMap<>(auditInstPayload));
}
return auditEntry;
}
}
@@ -0,0 +1,43 @@
package com.flowable.local.work.service;
import com.flowable.dataobject.api.runtime.DataObjectInstanceVariableContainer;
import com.flowable.dataobject.api.runtime.DataObjectInstanceVariableContainerBuilder;
import com.flowable.dataobject.api.runtime.DataObjectRuntimeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class DataObjectUtils {
protected final DataObjectRuntimeService dataObjectRuntimeService;
public DataObjectUtils(DataObjectRuntimeService dataObjectRuntimeService) {
this.dataObjectRuntimeService = dataObjectRuntimeService;
}
public void create(String tenantId, String definitionKey) {
DataObjectInstanceVariableContainerBuilder dataObjectValueInstanceBuilder = dataObjectRuntimeService.createDataObjectValueInstanceBuilder();
dataObjectValueInstanceBuilder.tenantId(tenantId);
dataObjectValueInstanceBuilder.definitionKey(definitionKey);
/*TODO: iterate list
for (entry : list) {
dataObjectValueInstanceBuilder.value(entry.key(), entry.value());
}
*/
dataObjectValueInstanceBuilder.create();
}
public void delete(String tenantId, String definitionKey) {
List<DataObjectInstanceVariableContainer> dataObjects = dataObjectRuntimeService.createDataObjectInstanceQuery()
.tenantId(tenantId)
.definitionKey(definitionKey)
.operation("searchAll")
.list();
for (DataObjectInstanceVariableContainer dataObjectInstanceVariableContainer : dataObjects) {
dataObjectRuntimeService.deleteDataObject(dataObjectInstanceVariableContainer.getLookupId().toString(),
dataObjectInstanceVariableContainer.getDefinitionId());
}
}
}
@@ -0,0 +1,107 @@
package com.flowable.local.work.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
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 com.github.wnameless.json.flattener.JsonFlattener;
import com.github.wnameless.json.unflattener.JsonUnflattener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@Component
public class JsonUtils {
protected static final Logger LOGGER = LoggerFactory.getLogger(JsonUtils.class);
protected final ObjectMapper objectMapper;
protected final JavaTimeModule javaTimeModule;
public JsonUtils(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.javaTimeModule = new JavaTimeModule();
// Enable ObjectMapper for handling Instant as string
this.objectMapper.registerModule(javaTimeModule);
this.objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
}
public JsonNode convertListToJsonNode(List<Object> list) {
return objectMapper.valueToTree(list);
}
public JsonNode convertMapToJsonNode(Map<String, Object> map) {
return objectMapper.valueToTree(map);
}
public Map<String, Object> convertJsonNodeToMap(JsonNode jsonNode) {
return objectMapper.convertValue(jsonNode, new TypeReference<Map<String, Object>>() {});
}
public Map<String, Object> flatten(Object payload) {
try {
return JsonFlattener.flattenAsMap(objectMapper.writeValueAsString(payload));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public Map<String, Object> unflatten(Map<String, Object> flatVars) {
try {
return objectMapper.readValue(JsonUnflattener.unflatten(flatVars), Map.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public JsonNode convertJsonStringToJsonNode(String jsonString) {
try {
return objectMapper.readTree(jsonString);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public Map<String, Object> convertObjectNodeToMap(ObjectNode objectNode) {
return objectMapper.convertValue(objectNode, new TypeReference<Map<String, Object>>() {});
}
public ObjectNode getEmptyObjectNode() {
return objectMapper.createObjectNode();
}
public ObjectNode loadObjectNodeFromFile(String path) {
try {
return (ObjectNode) this.objectMapper.readTree(new File(path));
} catch (IOException e) {
LOGGER.debug(e.getMessage());
return null;
}
}
public ArrayNode convertObjectToArrayNode(Object object) {
if (object == null) {
LOGGER.debug("{}: Argument is null", this.getClass().getName());
return null;
}
String obyTypeName = object.getClass().getName();
if (obyTypeName.equals("java.util.Collections$EmptyList") || obyTypeName.equals("java.util.ArrayList")) {
return objectMapper.convertValue(object, ArrayNode.class);
} else if (obyTypeName.equals("com.fasterxml.jackson.databind.node.ArrayNode")) {
return (ArrayNode) object;
} else {
LOGGER.debug("{}: {} is not implemented}", this.getClass().getName(), obyTypeName);
return null;
}
}
}
@@ -0,0 +1,77 @@
package com.flowable.local.work.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.flowable.action.api.bot.BaseBotActionResult;
import com.flowable.action.api.bot.BotActionResult;
import com.flowable.action.api.bot.BotService;
import com.flowable.action.api.history.HistoricActionInstance;
import com.flowable.action.api.intents.Intent;
import com.flowable.action.api.repository.ActionDefinition;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.runtime.ProcessInstance;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
public class StartProcessBot implements BotService {
protected static final Logger LOGGER = LoggerFactory.getLogger(StartProcessBot.class);
public static final String BOT_KEY = "start-process-bot";
public static final String BOT_NAME = "start process bot";
public static final String BOT_DESCRIPTION = "This bot starts a process.";
private final RuntimeService runtimeService;
private final ObjectMapper objectMapper;
private final HistoryService historyService;
public StartProcessBot(RuntimeService runtimeService,
ObjectMapper objectMapper,
HistoryService historyService) {
this.runtimeService = runtimeService;
this.objectMapper = objectMapper;
this.historyService = historyService;
}
@Override
public String getKey() { return BOT_KEY; }
@Override
public String getName() { return BOT_NAME; }
@Override
public String getDescription() { return BOT_DESCRIPTION; }
@Override
public BotActionResult invokeBot(HistoricActionInstance actionInstance, ActionDefinition actionDefinition, Map<String, Object> payload) {
LOGGER.info(getKey() + " starting...");
String rootCaseId = actionInstance.getScopeId();
if (rootCaseId == null) {
throw new RuntimeException(getKey() + " cannot get Scope Id from action button");
}
String taskModelId = null;
try {
taskModelId = payload.get("taskModelId").toString();
} catch (Exception e) {
}
if (taskModelId == null) {
throw new RuntimeException(getKey() + " cannot get taskModelId from payload");
}
LOGGER.info(getKey() + " starting task instance with taskModelId " + taskModelId + "...");
ProcessInstance processInstance = this.runtimeService.createProcessInstanceBuilder()
.processDefinitionKey(taskModelId)
.tenantId(actionInstance.getTenantId())
.variables(payload)
.start();
if (processInstance == null) {
throw new RuntimeException(getKey() + " cannot find enabled task instance with taskModelId " + taskModelId);
}
LOGGER.info(getKey() + " task instance with taskModelId " + taskModelId + " successfully started");
Map<String, Object> processVariables = processInstance.getProcessVariables();
ObjectNode responsePayload = this.objectMapper.convertValue(processVariables, ObjectNode.class);
responsePayload.put("id", processInstance.getId());
return new BaseBotActionResult(responsePayload, Intent.NOOP);
}
}
@@ -0,0 +1,101 @@
package com.flowable.local.work.service;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.flowable.core.common.api.security.SecurityScope;
import com.flowable.core.idm.api.*;
import com.flowable.core.spring.security.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.validation.constraints.NotNull;
import java.util.*;
import java.util.stream.Collectors;
/*
Returns user information:
- getCurrentUserGroups()
- getUserGroups(userId)
*/
@Component
public class UserService {
protected static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);
protected final PlatformIdentityService platformIdentityService;
public UserService(PlatformIdentityService platformIdentityService) {
this.platformIdentityService = platformIdentityService;
}
@NotNull
public ArrayNode getCurrentUserGroups() {
LOGGER.debug("Getting current user groups");
SecurityScope currentUserSecurityScope = SecurityUtils.getCurrentUserSecurityScopeSafe();
List<String> stringList = new ArrayList<>(currentUserSecurityScope.getGroupKeys());
return createSortedArrayNode(stringList);
}
@NotNull
protected static ArrayNode createSortedArrayNode(List<String> stringList) {
Collections.sort(stringList);
ArrayNode roles = JsonNodeFactory.instance.arrayNode();
for(String role : stringList) {
// Apply a role filter here if necessary
roles.add(role);
}
return roles;
}
@NotNull
public ArrayNode getUserGroups(String userId) {
LOGGER.debug("Getting groups of user {}", userId);
SecurityScope currentUserSecurityScope = SecurityUtils.getCurrentUserSecurityScopeSafe();
// Get groups via User Query
List<String> groupStringList = getLdapEntitlements(userId, currentUserSecurityScope.getTenantId());
if (groupStringList.isEmpty()) {
// Get groups via Group Query
groupStringList = getPlatformGroups(userId, currentUserSecurityScope);
}
return createSortedArrayNode(groupStringList);
}
@NotNull
protected List<String> getPlatformGroups(String userId, SecurityScope currentUserSecurityScope) {
List<PlatformGroup> platformGroups = new ArrayList<>();
try {
platformGroups = platformIdentityService.createPlatformGroupQuery()
.groupTenantId(currentUserSecurityScope.getTenantId())
.groupMember(userId)
.list();
} catch (Exception ignored) {
}
List<String> stringList = new ArrayList<>();
for(PlatformGroup platformGroup : platformGroups) {
stringList.add(platformGroup.getKey());
}
return stringList;
}
@NotNull
protected List<String> getLdapEntitlements(String userId, String tenantId) {
PlatformUser platformUser = platformIdentityService.createPlatformUserQuery()
.tenantId(tenantId)
.userId(userId)
.includeIdentityInfo()
.singleResult();
return Optional.ofNullable(platformUser)
.map(PlatformUser::getIdentityInfo)
.flatMap(identityInfos -> identityInfos.stream()
.filter(ifInfo -> "ldap_entitlement".equals(ifInfo.getName()))
.findFirst())
.map(PlatformIdentityInfo::getValue)
.filter(value -> value instanceof String) // Ensure that value is String
.map(value -> (String) value)
.map(value -> Arrays.stream(value.split(";"))
.filter(val -> !val.trim().isEmpty())
.collect(Collectors.toList()))
.orElseGet(List::of);
}
}
@@ -0,0 +1,37 @@
package com.flowable.local.work.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.flowable.cmmn.api.CmmnRuntimeService;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.*;
@Component
public class VariableService {
protected final ObjectMapper objectMapper;
protected final CmmnRuntimeService cmmnRuntimeService;
public VariableService(ObjectMapper objectMapper, CmmnRuntimeService cmmnRuntimeService) {
this.objectMapper = objectMapper;
this.objectMapper.registerModule(new JavaTimeModule());
this.objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
this.cmmnRuntimeService = cmmnRuntimeService;
}
public JsonNode getVariable(@PathVariable String caseInstanceId, String variableName) {
// Check parameters
Map<String, Object> map = new LinkedHashMap<>();
map.put("caseInstanceId", caseInstanceId);
map.put("variableName", variableName);
map.put("value", this.cmmnRuntimeService.getVariables(caseInstanceId));
return this.objectMapper.convertValue(map, JsonNode.class);
}
}
@@ -0,0 +1,8 @@
# == Embedded DB ==
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:file:./h2-database/flowable-work;DB_CLOSE_ON_EXIT=FALSE
# == Elasticless ==
flowable.indexing.enabled=false
management.health.elasticsearch.enabled=false
management.metrics.export.elastic.enabled=false
@@ -0,0 +1,11 @@
flowable.license.db-store-enabled=true
flowable.inspect.enabled=false
# Pretty-print JSON responses
spring.jackson.serialization.indent_output=false
# Forms will update even if an old process/case/task definition will be used
flowable.platform.enable-latest-form-definition-lookup=false
# Environment identifier
info.env.name=
# In order for hot swapping to work for the custom.js and custom.css
spring.thymeleaf.cache=true
spring.web.resources.chain.cache=true
@@ -0,0 +1,40 @@
server.port=8090
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/flowable
spring.datasource.username=flowable
spring.datasource.password=flowable
# Enable all endpoints over HTTP
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=ALWAYS
# Set this false, to disable metrics export to elasticsearch
# management.metrics.export.elastic.enabled=false
flowable.indexing.index-name-prefix=local-
flowable.security.impersonate.allowed=true
# Set this to true to get a clean IDM setup with only an admin user
flowable.platform.idm.minimal-setup=true
flowable.platform.idm.default-password=test
# == Development Properties ==
flowable.license.db-store-enabled=false
flowable.inspect.enabled=true
# Pretty-print JSON responses
spring.jackson.serialization.indent_output=true
# Forms will update even if an old process/case/task definition will be used
flowable.platform.enable-latest-form-definition-lookup=true
# Environment identifier
info.env.name=Development
# In order for hot swapping to work for the custom.js and custom.css
spring.thymeleaf.cache=false
spring.web.resources.chain.cache=false
# Path to auto-deploy-apps
flowable.app.resource-location=classpath*:/auto-deploy-apps/
# Email
flowable.mail.server.host=localhost
flowable.mail.server.port=2525
@@ -0,0 +1,21 @@
{
"key": "md-country",
"name": "Country",
"description": "The master data definition for countries.",
"dataObjectType": "masterData",
"type": "internal",
"subType": "country",
"sourceId": "JSON",
"supportsNameFiltering": true,
"keyField": "alpha3Code",
"idField": "alpha3Code",
"nameField": "name",
"variables": {
"alpha2Code": "alpha2Code",
"numericCode": "numeric"
}
}
@@ -0,0 +1,47 @@
{
"dataObjectDefinitionKey": "md-country",
"masterData": [
{
"name": "Australia default",
"alpha2Code": "AU",
"alpha3Code": "AUS",
"numeric": 36,
"translations": {
"de": {
"name": "Australien de"
},
"en": {
"name": "Australia en"
}
}
},
{
"name": "Bermuda default",
"alpha2Code": "BM",
"alpha3Code": "BMU",
"numeric": 60,
"translations": {
"de": {
"name": "Bermuda de"
},
"en": {
"name": "Bermuda en"
}
}
},
{
"name": "Haiti default",
"alpha2Code": "HT",
"alpha3Code": "HTI",
"numeric": 332,
"translations": {
"de": {
"name": "Haiti de"
},
"en": {
"name": "Haiti en"
}
}
}
]
}
@@ -0,0 +1,23 @@
package com.flowable.local.work;
import org.flowable.spring.impl.test.FlowableSpringExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
/**
* Because executing a Spring Boot Test will actually start Flowable and therefore additional
* infrastructure like elasticsearch is needed to run this test it is not a Unit Test but an
* Integration Test and should therefore not executed by maven surefire. Therefore the ending IT
* (for IntegrationTest) instead of Test.
*/
@SpringBootTest
@ExtendWith(FlowableSpringExtension.class)
//@Deployment(resources = {"my-process.bmpn20.xml"})
class FlowableLocalWorkApplicationIT {
@Test
void canStartApplication() {
}
}
@@ -0,0 +1,14 @@
package com.flowable.local.work;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
class FlowableLocalWorkApplicationTest {
@Test
void canStartContext() {
}
}
@@ -0,0 +1,7 @@
package com.flowable.local.work.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TestConfiguration {
}
@@ -0,0 +1,49 @@
package com.flowable.local.work.model;
import java.util.List;
public class EmailDto {
private String subject;
private List<String> receiverList;
private String content;
private Object contentRaw;
public EmailDto(String subject, List<String> receiverList, String content, Object contentRaw) {
this.subject = subject;
this.receiverList = receiverList;
this.content = content;
this.contentRaw = contentRaw;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public List<String> getReceiverList() {
return receiverList;
}
public void setReceiverList(List<String> receiverList) {
this.receiverList = receiverList;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Object getContentRaw() {
return contentRaw;
}
public void setContentRaw(Object contentRaw) {
this.contentRaw = contentRaw;
}
}
@@ -0,0 +1,335 @@
package com.flowable.local.work.model;
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.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
@Component
public class FlowableExcelMapper {
protected static ObjectMapper objectMapper = new ObjectMapper();
protected static JavaTimeModule javaTimeModule = new JavaTimeModule();
protected static FlowableExcelParser flowableExcelParser = new FlowableExcelParser();
public FlowableExcelMapper() {
// Enable ObjectMapper for handling Instant as string
objectMapper.registerModule(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
public ArrayNode excelBookResourceToJsonNode(String resourcePath) {
ArrayList<ArrayList<ArrayList<String>>> book = flowableExcelParser.parseExcelBookFromResource(resourcePath);
return bookToJsonNode(book);
}
protected ArrayNode bookToJsonNode(ArrayList<ArrayList<ArrayList<String>>> book) {
ArrayNode bookNode = objectMapper.createArrayNode();
for (ArrayList<ArrayList<String>> sheet : book) {
bookNode.add(sheetToJsonNode(sheet));
}
return bookNode;
}
protected ArrayNode sheetToJsonNode(ArrayList<ArrayList<String>> rows) {
ArrayList<String> paths = new ArrayList<>();
ArrayList<String> 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));
}
}
return arrayNode;
}
protected JsonNode rowToJsonNode(ArrayList<String> types, ArrayList<String> paths, ArrayList<String> row, JsonNode jsonNode) {
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);
}
}
jsonNode = addNode(jsonNode, paths.get(cellNum), content);
}
}
return jsonNode;
}
protected JsonNode addNode(JsonNode parent, String path, Object value) {
if (parent == null) {
parent = objectMapper.createObjectNode();
}
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));
} 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());
}
((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;
}
}
}
return -1;
}
public ArrayList<ArrayList<Object>> excelBookResourceToObj(String resourcePath) {
ArrayList<ArrayList<ArrayList<String>>> book = flowableExcelParser.parseExcelBookFromResource(resourcePath);
return bookToObj(book);
}
protected ArrayList<ArrayList<Object>> bookToObj(ArrayList<ArrayList<ArrayList<String>>> book) {
ArrayList<ArrayList<Object>> bookNode = new ArrayList<>();
for (ArrayList<ArrayList<String>> sheet : book) {
bookNode.add(sheetToObj(sheet));
}
return bookNode;
}
protected ArrayList<Object> sheetToObj(ArrayList<ArrayList<String>> rows) {
ArrayList<String> paths = new ArrayList<>();
ArrayList<String> types = new ArrayList<>();
ArrayList<Object> 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<String> types, ArrayList<String> paths, ArrayList<String> 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);
}
}
node = addObj(node, paths.get(cellNum), content);
}
}
return node;
}
@SuppressWarnings("unchecked")
protected Object addObj(Object parent, String path, Object value) {
if (parent == null) {
parent = new LinkedHashMap<String, Object>();
}
int delimiterPosition = indexOfFirstDelimiter(path, ".*");
if (delimiterPosition <= 0) {
if (parent instanceof LinkedHashMap) {
// Key of a value in an ObjectNode
((Map<String, Object>) parent).put(path, value);
} else if (parent instanceof ArrayList) {
// Index of a value in an ArrayNode
int index = Integer.parseInt(path);
while (((ArrayList<Object>) parent).size() <= index) {
// Create empty entries to parent ArrayNode
((ArrayList<Object>) parent).add(new LinkedHashMap<String, Object> ());
}
((ArrayList<Object>) parent).set(index, value);
} else {
throw new RuntimeException("Parent type not supported: " + parent.getClass().getName());
}
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<String, Object>();
} else if (delimiter.equals("*")) {
child = new ArrayList<>();
}
Object node;
if (parent instanceof LinkedHashMap) {
// Key of a JsonNode in an ObjectNode
node = ((Map<String, Object>) parent).get(key);
if (node != null) {
// Take the existing ObjectNode as child
child = node;
}
((Map<String, Object>) 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<Object>) parent).size() > index) {
node = ((ArrayList<Object>) parent).get(index);
} else {
node = null;
}
if (node != null) {
// Take the existing ArrayNode as child
child = node;
}
while (((ArrayList<Object>) parent).size() <= index) {
// Create empty entries to parent ArrayNode
((ArrayList<Object>) parent).add(new LinkedHashMap<String, Object> ());
}
((ArrayList<Object>) parent).set(index, addObj(child, newPath, value));
} else {
throw new RuntimeException("Parent type not supported: " + parent.getClass().getName());
}
return parent;
}
}
@@ -0,0 +1,138 @@
package com.flowable.local.work.model;
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.util.ArrayList;
import java.util.LinkedHashMap;
@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<ArrayList<ArrayList<String>>> parseExcelBookFromResource(String excelResourcePath) {
try (FileInputStream fileInputStream = new FileInputStream(getResourcePath(excelResourcePath))) {
Workbook workBook = new XSSFWorkbook(fileInputStream);
return parseBook(workBook);
} catch (IOException e) {
throw new RuntimeException(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<ArrayList<ArrayList<String>>> parseBook(Workbook workBook ) {
ArrayList<ArrayList<ArrayList<String>>> book = new ArrayList<>();
FormulaEvaluator formulaEvaluator = workBook.getCreationHelper().createFormulaEvaluator();
for (Sheet workSheet : workBook) {
book.add(parseSheet(workSheet, formulaEvaluator));
}
return book;
}
private ArrayList<ArrayList<String>> parseSheet(Sheet workSheet, FormulaEvaluator formulaEvaluator) {
ArrayList<ArrayList<String>> sheet = new ArrayList<>();
// Count columns of first row
int maxCols = getMaxCols(workSheet.getRow(0));
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<String> row = parseRow(workSheet.getRow(rowIndex), maxCols, formulaEvaluator, dataFormatter);
if (row == null) {
// Take all rows until the first empty row
break;
} else {
sheet.add(row);
}
}
return sheet;
}
private ArrayList<String> parseRow(Row workRow, int maxCols, FormulaEvaluator formulaEvaluator, DataFormatter dataFormatter) {
if (workRow == null) {
// Row is empty
return null;
}
ArrayList<String> row = new ArrayList<>();
boolean allCellsNull = 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;
}
row.add(content);
}
if (allCellsNull) {
// Row is empty
return null;
} else {
return row;
}
}
private int getMaxCols(Row workRow) {
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
return colIndex;
}
}
return maxCol;
}
@Deprecated
public ArrayList<LinkedHashMap<String, Object>> parseExcelFromResource(String resourcePath) {
try (FileInputStream fileInputStream = new FileInputStream(getResourcePath(resourcePath))) {
Workbook workBook = new XSSFWorkbook(fileInputStream);
FormulaEvaluator formulaEvaluator = workBook.getCreationHelper().createFormulaEvaluator();
ArrayList<ArrayList<String>> sheet = parseSheet(workBook.getSheetAt(0), formulaEvaluator);
ArrayList<String> colHeaders = new ArrayList<>();
ArrayList<LinkedHashMap<String, Object>> rowList = new ArrayList<>();
for (int rowIndex = 0; rowIndex < sheet.size(); rowIndex++) {
ArrayList<String> row = sheet.get(rowIndex);
if (rowIndex == 0) {
// Header row
for (Object cell : row) {
colHeaders.add(String.valueOf(cell));
}
continue;
}
LinkedHashMap<String, Object> 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);
}
}
}
@@ -0,0 +1,110 @@
package com.flowable.local.work.model;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
@Component
public class FlowableJsonParser {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final JavaTimeModule javaTimeModule = new JavaTimeModule();
public static final String BOOLEAN = "Boolean";
public static final String STRING = "String";
public static final String INTEGER = "Integer";
public static final String DOUBLE = "Double";
public static final String LONG = "Long";
public static final String INSTANT = "Instant";
public static final String JSON_OBJECT_NODE = "JsonObjectNode";
public static final String JSON_ARRAY_NODE = "JsonArrayNode";
public static final String MAP = "Map";
public static final String ARRAY_LIST = "ArrayList";
public FlowableJsonParser() {
objectMapper.registerModule(javaTimeModule);
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
}
public Map<String, Object> parseMap(Object map) throws JsonProcessingException, ClassCastException {
return (Map<String, Object>) parseObject(map);
}
public Object parseObject(Object object) throws JsonProcessingException, ClassCastException {
if (object instanceof Map) {
Map<String, Object> resultMap = new LinkedHashMap<>(Map.of());
for (Map.Entry<String, Object> mapEntry : ((Map<String, Object>) object).entrySet()) {
String key = mapEntry.getKey();
resultMap.put(key, parseMapEntry(key, mapEntry.getValue()));
}
return resultMap;
}
if (object instanceof ArrayList) {
ArrayList<Object> resultList = new ArrayList<>();
for (Object arrayEntry : (ArrayList<Object>) object) {
resultList.add(parseArrayEntry(arrayEntry));
}
return resultList;
}
return null;
}
private Object parseMapEntry(String key, Object value) throws JsonProcessingException {
if (value instanceof Map) {
Map<String, Object> valueMap = (Map<String, Object>) value;
if (valueMap.containsKey("__TYPE") && valueMap.containsKey("__VALUE") && valueMap.size() == 2) {
// if Map in Map, check if inner Map has explicit type (__TYPE and __VALUE)
return convertObjectExplicit(valueMap.get("__VALUE"), (String) valueMap.get("__TYPE"));
}
if (Arrays.asList("__IN", "__OUT").contains(key)) {
// convert __IN and __OUT maps as Map
return parseObject(value);
}
// convert Map to JsonObjectNode (default in models)
return convertObjectExplicit(value, JSON_OBJECT_NODE);
}
if (value instanceof ArrayList) {
// convert ArrayList to JsonArrayNode (default in models)
return convertObjectExplicit(value, JSON_ARRAY_NODE);
}
// take json supported type (Boolean, String, Integer, Double)
return value;
}
private Object parseArrayEntry(Object value) throws JsonProcessingException {
if (value instanceof Map) {
// convert Map to JsonObjectNode (default in models)
return convertObjectExplicit(value, JSON_OBJECT_NODE);
}
if (value instanceof ArrayList) {
// convert ArrayList to JsonArrayNode (default in models)
return convertObjectExplicit(value, JSON_ARRAY_NODE);
}
// take json supported type (Boolean, String, Integer, Double)
return value;
}
private Object convertObjectExplicit(Object value, String explicitType) throws JsonProcessingException {
switch (explicitType) {
case BOOLEAN: return Boolean.parseBoolean(value.toString());
case STRING: return value.toString();
case INTEGER: return Integer.parseInt(value.toString());
case DOUBLE: return Double.parseDouble(value.toString());
case LONG: return Long.parseLong(value.toString());
case INSTANT: return Instant.parse(value.toString());
case MAP: case ARRAY_LIST: return parseObject(value);
case JSON_OBJECT_NODE: case JSON_ARRAY_NODE: return objectMapper.convertValue(parseObject(value), JsonNode.class);
default: return value;
}
}
}
@@ -0,0 +1,54 @@
package com.flowable.local.work.model;
import com.flowable.action.engine.test.ActionExtension;
import com.flowable.app.engine.test.FlowableAppExtension;
import com.flowable.dataobject.engine.test.DataObjectExtension;
import com.flowable.form.spring.impl.test.FlowableFormSpringExtension;
import com.flowable.idm.engine.test.PlatformIdmExtension;
import com.flowable.platform.tenant.test.TenantSetupExtension;
import com.flowable.policy.engine.test.PolicyExtension;
import com.flowable.serviceregistry.engine.test.ServiceRegistryExtension;
import com.flowable.template.engine.test.TemplateExtension;
import org.flowable.cmmn.spring.impl.test.FlowableCmmnSpringExtension;
import org.flowable.dmn.spring.impl.test.FlowableDmnSpringExtension;
import org.flowable.spring.impl.test.FlowableSpringExtension;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ExtendWith(SpringExtension.class)
@ExtendWith(ActionExtension.class)
@ExtendWith(DataObjectExtension.class)
//@ExtendWith(EngageExtension.class)
@ExtendWith(FlowableAppExtension.class)
@ExtendWith(FlowableSpringExtension.class)
@ExtendWith(FlowableCmmnSpringExtension.class)
@ExtendWith(FlowableFormSpringExtension.class)
@ExtendWith(FlowableDmnSpringExtension.class)
@ExtendWith(PlatformIdmExtension.class)
@ExtendWith(PolicyExtension.class)
@ExtendWith(ServiceRegistryExtension.class)
@ExtendWith(TemplateExtension.class)
@ExtendWith(TenantSetupExtension.class)
@ExtendWith(TestMailServerExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Transactional
@SpringBootTest
public @interface FlowableModelTest {
/*
@AliasFor(annotation = SpringBootTest.class, attribute = "webEnvironment")
SpringBootTest.WebEnvironment webEnvironment() default SpringBootTest.WebEnvironment.MOCK;
*/
}
@@ -0,0 +1,655 @@
package com.flowable.local.work.model;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.*;
import com.flowable.audit.api.AuditService;
import com.flowable.audit.api.runtime.AuditInstance;
import com.flowable.core.spring.security.SecurityUtils;
import com.flowable.local.work.service.JsonUtils;
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;
import org.flowable.cmmn.api.runtime.CaseInstance;
import org.flowable.cmmn.engine.CmmnEngine;
import org.flowable.common.engine.api.identity.AuthenticationContext;
import org.flowable.common.engine.impl.identity.Authentication;
import org.flowable.engine.ManagementService;
import org.flowable.engine.ProcessEngine;
import org.flowable.engine.TaskService;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.job.service.impl.persistence.entity.TimerJobEntity;
import org.flowable.spring.security.SpringSecurityAuthenticationContext;
import org.flowable.task.api.Task;
import org.flowable.variable.api.history.HistoricVariableInstance;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.platform.commons.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.io.FileOutputStream;
import java.net.URI;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@Component
public class FlowableModelTestUtils {
protected final ProcessEngine processEngine;
protected final CmmnEngine cmmnEngine;
protected final TaskService taskService;
protected final PlatformTaskService platformTaskService;
protected final ManagementService managementService;
protected final JsonUtils jsonUtils;
protected final FlowableExcelMapper flowableExcelMapper;
protected static final Logger logger = LoggerFactory.getLogger(FlowableModelTestUtils.class);
protected final TestMailServer testMailServer;
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 FlowableModelTestUtils(ProcessEngine processEngine,
CmmnEngine cmmnEngine,
TaskService taskService,
PlatformTaskService platformTaskService,
ManagementService managementService,
JsonUtils jsonUtils,
FlowableExcelMapper flowableExcelMapper,
TestMailServer testMailServer,
AuditService auditService) {
this.processEngine = processEngine;
this.cmmnEngine = cmmnEngine;
this.taskService = taskService;
this.platformTaskService = platformTaskService;
this.managementService = managementService;
this.jsonUtils = jsonUtils;
this.flowableExcelMapper = flowableExcelMapper;
this.testMailServer = testMailServer;
this.auditService = auditService;
}
public void checkAndAndAssertAuditRecord(Map<String, Object> 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) {
assertAuditRecord(auditNumber, hint, message, category, type);
}
}
}
protected void assertAuditRecord(int auditNumber, String hint, String message, String category, String type) {
List<AuditInstance> auditTrail = getAuditTrail();
int auditTrailSize = auditTrail.size();
Assertions.assertThat(auditTrailSize).as(hint + ": invalid auditNumber " + auditTrailSize).isGreaterThanOrEqualTo(auditNumber);
if (auditTrailSize == 0) return;
AuditInstance auditInstance = auditTrail.get(auditNumber - 1);
Assertions.assertThat(auditInstance.getPayload().get("message")).as(hint).isEqualTo(message);
Assertions.assertThat(auditInstance.getPayload().get("category")).as(hint).isEqualTo(category);
Assertions.assertThat(auditInstance.getType()).as(hint).isEqualTo(type);
}
public void checkAndAssertEmail(Map<String, Object> map, int emailNumber, String hint, String subject, String receivers) {
if (map != null) {
Object check = map.get("email");
if (check instanceof Integer && (Integer) check > 0) {
assertEmail(emailNumber, hint, subject, receivers);
}
}
}
protected void assertEmail(int emailNumber, String hint, String subject, String receivers) {
List<EmailDto> emails = getMailList();
int emailListSize = emails.size();
Assertions.assertThat(emailListSize).as(hint + ": invalid email number " + emailListSize).isGreaterThanOrEqualTo(emailNumber);
if (emailNumber == 0) return;
EmailDto email = emails.get(emailNumber - 1);
String emailSubject = email.getSubject();
Assertions.assertThat(emailSubject).as(hint + ": invalid subject " + emailSubject).endsWith(subject);
List<Pair<String, Boolean>> receiverCheckList = new ArrayList<>();
for (String receiver : receivers.split("[,\\s]+")) {
receiverCheckList.add(Pair.of(receiver, false));
}
List<String> 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<String, Boolean> 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);
}
}
}
}
public Map<String, Object> getHistoryCasePayload(String caseInstanceId) {
List<HistoricVariableInstance> historicVariableInstanceList = cmmnEngine.getCmmnHistoryService()
.createHistoricVariableInstanceQuery()
.caseInstanceId(caseInstanceId)
.list();
return convertHistVariableListToMap(historicVariableInstanceList);
}
public Map<String, Object> getRuntimeCasePayload(String caseId) {
return cmmnEngine.getCmmnRuntimeService().getVariables(caseId);
}
public CaseInstance startCaseInstance(String key, Map<String, Object> variables) {
return cmmnEngine.getCmmnRuntimeService()
.createCaseInstanceBuilder()
.caseDefinitionKey(key)
.tenantId(TENANT_ID) // Must not be "default"
.variables(variables)
.start();
}
public ProcessInstance startProcessInstance(String key, Map<String, Object> variables) {
return processEngine.getRuntimeService()
.createProcessInstanceBuilder()
.processDefinitionKey(key)
.tenantId(TENANT_ID) // Must not be "default"
.variables(variables)
.start();
}
public Task getOpenTask(String taskKey) {
Task task = taskService.createTaskQuery().taskDefinitionKey(taskKey).singleResult();
Assertions.assertThat(task).as("No open task with key {} 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();
return task;
}
public void claimOpenTask(String taskKey, String userId) {
setTestAuthenticatedUser(userId, TENANT_ID);
taskService.claim(getOpenTask(taskKey).getId(), userId);
}
public List<AuditInstance> getAuditTrail() {
return auditService.createAuditInstanceQuery()
//.tenantId(TENANT_ID) TODO: Must not be null, should work with default tenant
.list();
}
public List<EmailDto> getMailList() {
return Arrays.stream(testMailServer.getMessages()).map(mimeMessage -> {
try {
return new EmailDto(mimeMessage.getSubject(), Arrays.stream(mimeMessage.getAllRecipients())
.map(Address::toString).collect(Collectors.toList()), mimeMessage.getContent().toString(), mimeMessage.getContent());
} catch (Exception e) {
throw new RuntimeException(e);
}
}).collect(Collectors.toList());
}
public boolean isSubset(JsonNode root, JsonNode test) {
// If test is null, it is always a subset of root
if (test == null || test.isNull()) {
return true;
}
// If test is a value node, compare values
if (test.isValueNode()) {
if (test.isTextual() && "__N_A".equals(test.asText())) {
return false;
}
if (root == null) {
return false;
}
return root.isValueNode() && root.asText().equals(test.asText());
}
// If test is an array node, check if all elements of test exist in root
if(test.isArray()){
for (int i = 0; i < test.size(); i++) {
JsonNode testElement = test.get(i);
JsonNode rootElement = null;
if (root != null) {
rootElement = root.get(i);
}
if (testElement.isTextual() && "__N_A".equals(testElement.asText())) {
if (rootElement != null && !rootElement.isNull()) {
return false;
}
continue;
}
if (!isSubset(rootElement, testElement)) {
return false;
}
}
return true;
}
// If test is an object node, check if all fields in test exist in root
if (test.isObject()){
for (Iterator<String> iterator = test.fieldNames(); iterator.hasNext(); ) {
String fieldName = iterator.next();
JsonNode testValue = test.get(fieldName);
JsonNode rootValue = null;
if (root != null) {
rootValue = root.get(fieldName);
}
if (testValue.isTextual() && "__N_A".equals(testValue.asText())) {
if (root != null && root.has(fieldName)) {
return false;
}
continue;
}
if (!isSubset(rootValue, testValue)){
return false;
}
}
return true;
}
// If none of the above matches, return false
return false;
}
public Stream<Arguments> getJsonArgumentsFromExcel(String path) {
ArrayList<Arguments> argumentList = new ArrayList<>();
JsonNode book = flowableExcelMapper.excelBookResourceToJsonNode(path);
for (JsonNode sheet : book) {
for (JsonNode row : sheet) {
argumentList.add(Arguments.of(path, row));
}
}
return argumentList.stream();
}
public Stream<Arguments> getObjArgumentsFromExcel(String path) {
ArrayList<Arguments> argumentList = new ArrayList<>();
ArrayList<ArrayList<Object>> book = flowableExcelMapper.excelBookResourceToObj(path);
for (ArrayList<Object> sheet : book) {
for (Object row : sheet) {
argumentList.add(Arguments.of(path, row));
}
}
return argumentList.stream();
}
public ObjectNode testJsonExcelRow(String path, JsonNode row) {
ObjectNode vars = jsonUtils.getEmptyObjectNode();
JsonNode rootParam = row.get("root");
if (rootParam != null) {
Iterator<Map.Entry<String, JsonNode>> rootVars = rootParam.fields();
while (rootVars.hasNext()) {
Map.Entry<String, JsonNode> rootVar = rootVars.next();
String key = rootVar.getKey();
JsonNode value = rootVar.getValue();
vars.set(key, value);
}
}
JsonNode inParam = row.get("in");
if (inParam != null) {
vars.set("__IN", inParam);
}
JsonNode outParam = row.get("out");
if (outParam != null) {
vars.set("__OUT", outParam);
}
JsonNode idParam = row.get("id");
JsonNode test = row.get("test");
int timerCount = 0;
JsonNode timerNode = row.get("timer");
if (timerNode != null) timerCount = timerNode.asInt();
Integer auditRecordCount = null;
JsonNode auditNode = row.get("audit");
if (auditNode != null) auditRecordCount = auditNode.asInt();
Integer emailCount = null;
JsonNode emailNode = row.get("email");
if (emailNode != null) emailCount = emailNode.asInt();
logger.info("TestExcelRow file={} id={} rootParam={} inParam={} outParam={} timer={} test={} audit={} email={} ",
path, idParam, rootParam, inParam, outParam, timerCount, test, auditRecordCount, emailCount);
ObjectNode processIds = createRootTestProcessInstance(idParam.asText(), jsonUtils.convertJsonNodeToMap(vars));
for (int i = 0; i < timerCount; i++) {
executeTimer(processIds.get(TEST_PROCESS_ID).asText());
}
ObjectNode result = jsonUtils.getEmptyObjectNode();
Map<String, Object> rootProcessPayload = getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText());
JsonNode root = jsonUtils.convertMapToJsonNode(rootProcessPayload);
if (root != null) {
Assertions.assertThat(isSubset(root, test))
.withFailMessage(System.lineSeparator() + "test :" + test + System.lineSeparator() + "root :" + root).isTrue();
result.set("root", root);
}
if (auditRecordCount != null) {
// List<AuditInstance> auditTrail = getAuditTrail(processIds.get(ROOT_PROCESS_ID).asText());
List<AuditInstance> auditTrail = getAuditTrail();
int auditTrailSize = auditTrail.size();
Assertions.assertThat(auditTrailSize).as("Invalid audit trail size: {}", auditTrailSize).isEqualTo(auditRecordCount);
result.set("auditTrail", jsonUtils.convertListToJsonNode(Collections.singletonList(auditTrail)));
}
if (emailCount != null) {
List<EmailDto> emailList = getMailList();
int emailListSize = emailList.size();
Assertions.assertThat(emailListSize).as("Invalid number of emails: {}", emailListSize).isEqualTo(emailCount);
result.set("emails", jsonUtils.convertListToJsonNode(Collections.singletonList(emailList)));
}
return result;
}
public Map<String, Object> testObjExcelRow(String path, Map<String, Object> row) {
Map<String, Object> vars = new LinkedHashMap<>();
Map<String, Object> rootParam = (Map<String, Object>) row.get("root");
if (rootParam != null) {
for (Map.Entry<String, Object> entry : rootParam.entrySet()) {
vars.put(entry.getKey(), entry.getValue());
}
}
Map<String, Object> inParam = (Map<String, Object>) row.get("in");
if (inParam != null) {
vars.put("__IN", inParam);
}
Map<String, Object> outParam = (Map<String, Object>) row.get("out");
if (outParam != null) {
vars.put("__OUT", outParam);
}
String idParam = (String) row.get("id");
Map<String, Object> test =(Map<String, Object>) row.get("test");
int timerCount = 0;
Object timerNode = row.get("timer");
if (timerNode != null) timerCount = (Integer) timerNode;
Integer auditRecordCount = null;
Object auditNode = row.get("audit");
if (auditNode != null) auditRecordCount = (Integer) auditNode;
Integer emailCount = null;
Object emailNode = row.get("email");
if (emailNode != null) emailCount = (Integer) emailNode;
logger.info("TestExcelRow file={} id={} rootParam={} inParam={} outParam={} timer={} test={} audit={} email={} ",
path, idParam, rootParam, inParam, outParam, timerCount, test, auditRecordCount, emailCount);
ObjectNode processIds = createRootTestProcessInstance(idParam, vars);
for (int i = 0; i < timerCount; i++) {
executeTimer(processIds.get(TEST_PROCESS_ID).asText());
}
Map<String, Object> result = new LinkedHashMap<>();
Map<String, Object> rootProcessPayload = getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText());
JsonNode root = jsonUtils.convertMapToJsonNode(rootProcessPayload);
if (root != null) {
Assertions.assertThat(isSubset(root, jsonUtils.convertMapToJsonNode(test)))
.withFailMessage(System.lineSeparator() + "test :" + test + System.lineSeparator() + "root :" + root).isTrue();
result.put("root", rootProcessPayload);
}
if (auditRecordCount != null) {
// List<AuditInstance> auditTrail = getAuditTrail(processIds.get(ROOT_PROCESS_ID).asText());
List<AuditInstance> auditTrail = getAuditTrail();
int auditTrailSize = auditTrail.size();
Assertions.assertThat(auditTrailSize).as("Invalid audit trail size: {}", auditTrailSize).isEqualTo(auditRecordCount);
result.put("audit", auditRecordCount);
}
if (emailCount != null) {
List<EmailDto> emailList = getMailList();
int emailListSize = emailList.size();
Assertions.assertThat(emailListSize).as("Invalid number of emails: {}", emailListSize).isEqualTo(emailCount);
result.put("email", emailCount);
}
return result;
}
public void exportApp(String rootUrl, String appModelKey, String username, String password) {
RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
RestTemplate restTemplate = restTemplateBuilder
.basicAuthentication(username, password)
.rootUri(rootUrl)
.build();
ResponseEntity<JsonNode> responseEntity = restTemplate.postForEntity(URI.create(rootUrl + "/app/authentication?j_username=" + username + "&j_password=" + password + "&spring_security_remember_me=true&submit=Login"), null, JsonNode.class);
String designCookieRaw = responseEntity.getHeaders().get("Set-Cookie").stream().collect(Collectors.joining(";"));
String flowableDesignRememberMeTokenValue = Arrays.stream(designCookieRaw.split(";")).filter(part -> part.contains("FLOWABLE_DESIGN_REMEMBER_ME")).findAny().get();
String csrfToken = Arrays.stream(designCookieRaw.split(";")).filter(part -> part.contains("FLOWABLE_DESIGN_CSRF_TOKEN")).findAny().get();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("Cookie", flowableDesignRememberMeTokenValue + ";" + csrfToken);
ArrayNode apps = (ArrayNode) restTemplate.exchange(URI.create(rootUrl + "/app/models?filter=apps&modelType=3&sort=modifiedDesc"), HttpMethod.GET, new HttpEntity<>(httpHeaders), JsonNode.class).getBody().path("data");
String appModelId = StreamSupport.stream(apps.spliterator(), false).filter(app -> app.path("key").asText().equals(appModelKey)).map( app -> app.path("id").asText()).findAny().get();
File file = new File(Paths.get("src/test/resources/test-auto-deploy-apps", appModelKey + ".zip").toString());
restTemplate.execute(
URI.create(rootUrl + "/app/app-definitions/" + appModelId + "/export-bar?includeChildReferences=true"),
HttpMethod.GET,
clientHttpRequest -> clientHttpRequest.getHeaders().set(
"Cookie", flowableDesignRememberMeTokenValue + ";" + csrfToken),
clientHttpResponse -> {
StreamUtils.copy(clientHttpResponse.getBody(), new FileOutputStream(file, false));
return file;
});
}
public ObjectNode emptyNode() {
return jsonUtils.getEmptyObjectNode();
}
public Map<String, Object> emptyMap() {
return new LinkedHashMap<>();
}
public ObjectNode loadObjectNodeFromResources(String path) {
if (path.isEmpty()) return jsonUtils.getEmptyObjectNode();
else return jsonUtils.loadObjectNodeFromFile(Paths.get("src","test", "resources", path).toString());
}
public Map<String, Object> loadMapFromResources(String path) {
return jsonUtils.convertJsonNodeToMap(loadObjectNodeFromResources(path));
}
public void executeTimer(String processId) {
TimerJobEntity timerJobEntity = (TimerJobEntity) managementService.createTimerJobQuery().processInstanceId(processId).singleResult();
Assertions.assertThat(timerJobEntity).withFailMessage("No active timer").isNotNull();
managementService.moveTimerToExecutableJob(timerJobEntity.getId());
managementService.executeJob(timerJobEntity.getId());
}
public void setTestAuthenticatedUser(String userId, String tenantId) {
if (userId == null) {
this.resetTestAuthenticatedUser();
} else {
List<GrantedAuthority> 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();
} else {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
if (StringUtils.isNotBlank(tenantId)) {
grantedAuthorities.add(SecurityUtils.createTenantAuthority(tenantId));
}
for (String groupKey : groupKeys) {
grantedAuthorities.add(SecurityUtils.createGroupAuthority(groupKey));
}
SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken(userId, "", grantedAuthorities));
AuthenticationContext authenticationContext = Authentication.getAuthenticationContext();
if (!(authenticationContext instanceof SpringSecurityAuthenticationContext)) {
Authentication.setAuthenticatedUserId(userId);
}
}
}
public void resetTestAuthenticatedUser() {
SecurityContextHolder.getContext().setAuthentication(null);
AuthenticationContext authenticationContext = Authentication.getAuthenticationContext();
if (!(authenticationContext instanceof SpringSecurityAuthenticationContext)) {
Authentication.setAuthenticatedUserId(null);
}
}
public void completeTaskWithFlatVars(String taskId, Map<String, Object> variables, String outcome) {
Map<String, Object> taskVariables = platformTaskService.getTaskVariables(taskId);
Map<String, Object> taskVariablesFlat = jsonUtils.flatten(taskVariables);
taskVariablesFlat.putAll(variables);
Map<String, Object> completionVars = jsonUtils.unflatten(taskVariablesFlat);
completeTask(taskId, completionVars, outcome);
}
public void completeTask(String taskId, Map<String, Object> completionVariables, String outcome) {
CompleteFormRepresentation form = new CompleteFormRepresentation();
for (String key : completionVariables.keySet()) {
form.setValues(key, completionVariables.get(key));
}
form.setOutcome(outcome);
platformTaskService.completeTaskForm(taskId, form);
}
public void completeOpenTask(String taskKey, ObjectNode vars, String outcome) {
Task task = getOpenTask(taskKey);
Map<String, Object> flatVars = jsonUtils.convertJsonNodeToMap(vars);
completeTaskWithFlatVars(task.getId(), flatVars, outcome);
}
public Map<String, Object> getHistProcessPayload(String processInstanceId) {
List<HistoricVariableInstance> historicVariableInstanceList = processEngine.getHistoryService()
.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.list();
return convertHistVariableListToMap(historicVariableInstanceList);
}
private HashMap<String, Object> convertHistVariableListToMap(List<HistoricVariableInstance> historicVariableInstanceList) {
HashMap<String, Object> variableMap = new HashMap<>();
for (HistoricVariableInstance historicVariableInstance : historicVariableInstanceList) {
variableMap.put(historicVariableInstance.getVariableName(), historicVariableInstance.getValue());
}
return variableMap;
}
public ObjectNode createRootTestProcessInstance(String testKey, Map<String, Object> variables) {
// Create root process with test process as call activity
String wrapperProcessKey = testKey + "_T";
BpmnModel bpmnModel = createWrapperTestProcessModel(testKey, (ObjectNode) jsonUtils.convertMapToJsonNode(variables));
processEngine.getProcessEngineConfiguration()
.getRepositoryService()
.createDeployment()
.addBpmnModel(wrapperProcessKey + ".bpmn20.xml", bpmnModel)
.tenantId(TENANT_ID) // Must not be "default"
.deploy();
// Start root process
ProcessInstance rootProcessInstance = startProcessInstance(wrapperProcessKey, variables);
// Store the process IDs for using them in assertions
ProcessInstance runtimeTestProcessInstance = processEngine.getRuntimeService()
.createProcessInstanceQuery()
.superProcessInstanceId(rootProcessInstance.getId())
.singleResult();
String testProcessId;
if (runtimeTestProcessInstance != null) {
testProcessId = runtimeTestProcessInstance.getId();
} else {
HistoricProcessInstance historicTestProcessInstance = processEngine.getHistoryService()
.createHistoricProcessInstanceQuery()
.superProcessInstanceId(rootProcessInstance.getId())
.singleResult();
// if history level is none, testProcessId is not available
testProcessId = historicTestProcessInstance != null ? historicTestProcessInstance.getId() : null;
}
ObjectNode rootNode = jsonUtils.getEmptyObjectNode();
rootNode.put(ROOT_PROCESS_ID, rootProcessInstance.getProcessInstanceId());
rootNode.put(TEST_PROCESS_ID, testProcessId);
return rootNode;
}
protected BpmnModel createWrapperTestProcessModel(String testKey, ObjectNode variables) {
BpmnModel bpmnModel = new BpmnModel();
Process process = new Process();
process.setId(testKey + "_T");
process.setName(testKey + "_T");
StartEvent startEvent = new StartEvent();
startEvent.setId(testKey + "_Start");
startEvent.setName(testKey + "_Start");
process.addFlowElement(startEvent);
CallActivity callActivity = new CallActivity();
callActivity.setId(testKey);
callActivity.setCalledElement(testKey);
JsonNode inNode = variables.get("__IN");
if (inNode instanceof ObjectNode) {
Map<String, Object> inMap = jsonUtils.convertObjectNodeToMap((ObjectNode) inNode);
ArrayList<IOParameter> inParameters = new ArrayList<>();
for (Map.Entry<String, Object> entry : inMap.entrySet()) {
IOParameter ioParameter = new IOParameter();
ioParameter.setSourceExpression("${__IN." + entry.getKey() + "}");
ioParameter.setTarget(entry.getKey());
inParameters.add(ioParameter);
}
callActivity.setInParameters(inParameters);
}
JsonNode outNode = variables.get("__OUT");
if (outNode instanceof ObjectNode) {
Map<String, String> outMap = (Map) jsonUtils.convertJsonNodeToMap(variables).get("__OUT");
ArrayList<IOParameter> outParameters = new ArrayList<>();
for (Map.Entry<String, String> entry : outMap.entrySet()) {
IOParameter ioParameter = new IOParameter();
ioParameter.setSource(entry.getKey());
ioParameter.setTarget(entry.getValue());
outParameters.add(ioParameter);
}
callActivity.setOutParameters(outParameters);
}
process.addFlowElement(callActivity);
EndEvent endEvent = new EndEvent();
endEvent.setId(testKey + "_End");
endEvent.setName(testKey + "_End");
process.addFlowElement(endEvent);
SequenceFlow start = new SequenceFlow(testKey + "_Start", testKey);
start.setId("Start_Sequence_Flow");
process.addFlowElement(start);
SequenceFlow end = new SequenceFlow(testKey, testKey + "_End");
end.setId("End_Sequence_Flow");
process.addFlowElement(end);
bpmnModel.addProcess(process);
return bpmnModel;
}
}
@@ -0,0 +1,33 @@
package com.flowable.local.work.model;
import com.flowable.spring.boot.properties.FlowableMailProperties;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
import jakarta.mail.internet.MimeMessage;
import org.springframework.stereotype.Component;
import static com.icegreen.greenmail.util.ServerSetup.PROTOCOL_SMTP;
@Component
public class TestMailServer {
private GreenMail greenMail;
private final FlowableMailProperties flowableMailProperties;
public TestMailServer(FlowableMailProperties flowableMailProperties) {
this.flowableMailProperties = flowableMailProperties;
}
public void setup() {
greenMail = new GreenMail(new ServerSetup(flowableMailProperties.getPort(), flowableMailProperties.getHost(), PROTOCOL_SMTP));
greenMail.start();
}
public void stop() {
greenMail.stop();
}
public MimeMessage[] getMessages() {
return greenMail.getReceivedMessages();
}
}
@@ -0,0 +1,22 @@
package com.flowable.local.work.model;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;
public class TestMailServerExtension implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
getTestMailServer(context).setup();
}
@Override
public void afterEach(ExtensionContext context) {
getTestMailServer(context).stop();
}
protected TestMailServer getTestMailServer(ExtensionContext context) {
return SpringExtension.getApplicationContext(context).getBean(TestMailServer.class);
}
}
@@ -0,0 +1,122 @@
package com.flowable.local.work.model.test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.*;
import com.flowable.local.work.model.EmailDto;
import com.flowable.local.work.model.FlowableModelTest;
import com.flowable.local.work.model.FlowableModelTestUtils;
import org.assertj.core.api.Assertions;
import org.flowable.cmmn.api.runtime.CaseInstance;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
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.*;
import java.util.stream.Stream;
@FlowableModelTest
public class ModelTest {
@Autowired
protected FlowableModelTestUtils flowableModelTest;
@Test
@Disabled
public void exportApp() {
flowableModelTest.exportApp("http://localhost:8093", "TST_APP", "admin", "test");
}
@Test
public void c001Test() {
flowableModelTest.setTestAuthenticatedUser("admin", null);
CaseInstance caseInstance = flowableModelTest.startCaseInstance("TST_C001", Map.of());
String caseInstanceId = caseInstance.getId();
flowableModelTest.completeOpenTask("TST_P001_T001", flowableModelTest.loadObjectNodeFromResources("model/test/C001/T001.json"), "COMPLETE");
Assertions.assertThat(flowableModelTest.getRuntimeCasePayload(caseInstanceId).get("testText")).isEqualTo("my test text");
flowableModelTest.completeOpenTask("TST_C001_T002", flowableModelTest.emptyNode(), "COMPLETE");
Assertions.assertThat(flowableModelTest.getHistoryCasePayload(caseInstanceId).get("testText")).isEqualTo("my test text");
}
@Test
public void p005HappyPathTest() {
ObjectNode processIds = flowableModelTest.createRootTestProcessInstance("TST_P005", flowableModelTest.emptyMap());
flowableModelTest.claimOpenTask("TST_P005_T001", "admin");
flowableModelTest.completeOpenTask("TST_P005_T001", flowableModelTest.loadObjectNodeFromResources("model/test/P005/T001.json"), null);
Map<String, Object> rootProcessPayload = flowableModelTest.getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText());
Assertions.assertThat(rootProcessPayload.get("dataEntry")).isEqualTo("my root text");
}
@NotNull
private Stream<Arguments> p001TestData() {
return Stream.of(
Arguments.of("admin", "model/test/P001/initiator.json")
);
}
@ParameterizedTest
@MethodSource("p001TestData")
public void p001Test(String initiator, String path) {
flowableModelTest.setTestAuthenticatedUser(initiator, null);
ObjectNode processIds = flowableModelTest.createRootTestProcessInstance("TST_P001",
flowableModelTest.loadMapFromResources(path));
flowableModelTest.completeOpenTask("TST_P001_T001", flowableModelTest.emptyNode(), "complete");
Map<String, Object> rootProcessPayload = flowableModelTest.getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText());
Assertions.assertThat(rootProcessPayload.get("rootResult")).isEqualTo(null);
}
@NotNull
private Stream<Arguments> p002TestData() {
return Stream.of(
Arguments.of("model/test/P002/boolean1.json", true, false),
Arguments.of("model/test/P002/boolean2.json", false, true),
Arguments.of("model/test/P002/string1.json", "hello root", "hello"),
Arguments.of("model/test/P002/string2.json", "123", "456"),
Arguments.of("model/test/P002/string3.json", "123.456,", "456.789"),
Arguments.of("model/test/P002/int.json", 123, 456),
Arguments.of("model/test/P002/double.json", 123.456, 456.789),
Arguments.of("model/test/P002/date.json", "2025-11-12", "2025-11-11")
);
}
@ParameterizedTest
@MethodSource("p002TestData")
public void p002Test(String path, Object result, Object out) {
ObjectNode processIds = flowableModelTest.createRootTestProcessInstance("TST_P002",
flowableModelTest.loadMapFromResources(path));
Map<String, Object> rootProcessPayload = flowableModelTest.getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText());
List<EmailDto> mail = flowableModelTest.getMailList();
Assertions.assertThat(mail.size()).isEqualTo(0);
Assertions.assertThat(rootProcessPayload.get("result")).isEqualTo(result);
Assertions.assertThat(rootProcessPayload.get("out")).isEqualTo(out);
}
private Stream<Arguments> p002ExcelTestData() {
return flowableModelTest.getJsonArgumentsFromExcel("model/test/P002/p002Test.xlsx");
}
@ParameterizedTest
@MethodSource("p002ExcelTestData")
public void p002ExcelTest(String path, JsonNode argument) {
ObjectNode result = flowableModelTest.testJsonExcelRow(path, argument);
}
private Stream<Arguments> p002ExcelTestData2() {
return flowableModelTest.getObjArgumentsFromExcel("model/test/P002/p002Test.xlsx");
}
@ParameterizedTest
@MethodSource("p002ExcelTestData2")
public void p002ExcelTest2(String path, Map<String, Object> argument) {
Map<String, Object> result = flowableModelTest.testObjExcelRow(path, argument);
flowableModelTest.checkAndAssertEmail(result, 1,
"Email", "Test", "test@flowable.com");
flowableModelTest.checkAndAndAssertAuditRecord(result, 1,
"Audit record", "TST_P002 Audit trail entry", "system", null);
}
}
@@ -0,0 +1,24 @@
spring.datasource.url=jdbc:h2:mem:flowable-work-db;DB_CLOSE_DELAY=1000
spring.datasource.username=admin
spring.datasource.password=test
# To make our life easier in tests we are disabling the async executor and elasticsearch
flowable.async-executor-activate=false
flowable.async-history-executor-activate=false
flowable.indexing.enabled=false
management.health.elasticsearch.enabled=false
management.metrics.export.elastic.enabled=false
# Set debug level in tests
logging.level.com.flowable=INFO
logging.level.com.flowable.local.work.model.FlowableModelTestUtils=INFO
# Disable the timeout process in the tests
flowable.external-system.wechat.timeout.process-definition-key=
# Path to test-auto-deploy-apps
flowable.app.resource-location=classpath*:/test-auto-deploy-apps/
# Email
flowable.mail.server.host=localhost
flowable.mail.server.port=3025
@@ -0,0 +1,5 @@
{
"root": {
"testText": "my test text"
}
}
@@ -0,0 +1,5 @@
{
"__IN": {
"initiator": "admin"
}
}
@@ -0,0 +1,9 @@
{
"param": true,
"__IN": {
"param": false
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": false,
"__IN": {
"param": true
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": "2025-11-12",
"__IN": {
"param": "2025-11-11"
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": 123.456,
"__IN": {
"param": 456.789
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": 123,
"__IN": {
"param": 456
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": "hello root",
"__IN": {
"param": "hello"
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": "123",
"__IN": {
"param": "456"
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,9 @@
{
"param": "123.456,",
"__IN": {
"param": "456.789"
},
"__OUT": {
"result": "out"
}
}
@@ -0,0 +1,5 @@
{
"root": {
"dataEntry": "my root text"
}
}