TSK-1628: New DmnTaskRouter module

This commit is contained in:
Joerg Heffner 2021-04-14 15:42:23 +02:00 committed by Mustapha Zorgati
parent 9247e70092
commit 3ddcd2ae97
23 changed files with 778 additions and 49 deletions

View File

@ -0,0 +1,194 @@
package pro.taskana.common.test.config;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Integration Test for TaskanaEngineConfiguration. */
public final class TaskanaEngineTestConfiguration {
private static final Logger LOGGER =
LoggerFactory.getLogger(TaskanaEngineTestConfiguration.class);
private static final int POOL_TIME_TO_WAIT = 50;
private static final DataSource DATA_SOURCE;
private static String schemaName = null;
static {
String userHomeDirectroy = System.getProperty("user.home");
String propertiesFileName = userHomeDirectroy + "/taskanaUnitTest.properties";
File f = new File(propertiesFileName);
if (f.exists() && !f.isDirectory()) {
DATA_SOURCE = createDataSourceFromProperties(propertiesFileName);
} else {
DATA_SOURCE = createDefaultDataSource();
}
}
private TaskanaEngineTestConfiguration() {}
/**
* returns the Datasource used for Junit test. If the file {user.home}/taskanaUnitTest.properties
* is present, the Datasource is created according to the properties jdbcDriver, jdbcUrl,
* dbUserName and dbPassword. Assuming, the database has the name tskdb, a sample properties file
* for DB2 looks as follows: jdbcDriver=com.ibm.db2.jcc.DB2Driver
* jdbcUrl=jdbc:db2://localhost:50000/tskdb dbUserName=db2user dbPassword=db2password If any of
* these properties is missing, or the file doesn't exist, the default Datasource for h2 in-memory
* db is created.
*
* @return dataSource for unit test
*/
public static DataSource getDataSource() {
return DATA_SOURCE;
}
/**
* returns the SchemaName used for Junit test. If the file {user.home}/taskanaUnitTest.properties
* is present, the SchemaName is created according to the property schemaName. a sample properties
* file for DB2 looks as follows: jdbcDriver=com.ibm.db2.jcc.DB2Driver
* jdbcUrl=jdbc:db2://localhost:50000/tskdb dbUserName=db2user dbPassword=db2password
* schemaName=TASKANA If any of these properties is missing, or the file doesn't exist, the
* default schemaName TASKANA is created used.
*
* @return String for unit test
*/
public static String getSchemaName() {
if (schemaName == null) {
String userHomeDirectroy = System.getProperty("user.home");
String propertiesFileName = userHomeDirectroy + "/taskanaUnitTest.properties";
File f = new File(propertiesFileName);
if (f.exists() && !f.isDirectory()) {
schemaName = getSchemaNameFromPropertiesObject(propertiesFileName);
} else {
schemaName = "TASKANA";
}
}
return schemaName;
}
/**
* create data source from properties file.
*
* @param propertiesFileName the name of the property file
* @return the parsed datasource.
*/
public static DataSource createDataSourceFromProperties(String propertiesFileName) {
DataSource ds;
try (InputStream input = new FileInputStream(propertiesFileName)) {
Properties prop = new Properties();
prop.load(input);
boolean propertiesFileIsComplete = true;
String warningMessage = "";
String jdbcDriver = prop.getProperty("jdbcDriver");
if (jdbcDriver == null || jdbcDriver.length() == 0) {
propertiesFileIsComplete = false;
warningMessage += ", jdbcDriver property missing";
}
String jdbcUrl = prop.getProperty("jdbcUrl");
if (jdbcUrl == null || jdbcUrl.length() == 0) {
propertiesFileIsComplete = false;
warningMessage += ", jdbcUrl property missing";
}
String dbUserName = prop.getProperty("dbUserName");
if (dbUserName == null || dbUserName.length() == 0) {
propertiesFileIsComplete = false;
warningMessage += ", dbUserName property missing";
}
String dbPassword = prop.getProperty("dbPassword");
if (dbPassword == null || dbPassword.length() == 0) {
propertiesFileIsComplete = false;
warningMessage += ", dbPassword property missing";
}
if (propertiesFileIsComplete) {
ds =
new PooledDataSource(
Thread.currentThread().getContextClassLoader(),
jdbcDriver,
jdbcUrl,
dbUserName,
dbPassword);
((PooledDataSource) ds)
.forceCloseAll(); // otherwise the MyBatis pool is not initialized correctly
} else {
LOGGER.warn("propertiesFile " + propertiesFileName + " is incomplete" + warningMessage);
LOGGER.warn("Using default Datasource for Test");
ds = createDefaultDataSource();
}
} catch (IOException e) {
LOGGER.warn("createDataSourceFromProperties caught Exception " + e);
LOGGER.warn("Using default Datasource for Test");
ds = createDefaultDataSource();
}
return ds;
}
static String getSchemaNameFromPropertiesObject(String propertiesFileName) {
String schemaName = "TASKANA";
try (InputStream input = new FileInputStream(propertiesFileName)) {
Properties prop = new Properties();
prop.load(input);
boolean propertiesFileIsComplete = true;
String warningMessage = "";
schemaName = prop.getProperty("schemaName");
if (schemaName == null || schemaName.length() == 0) {
propertiesFileIsComplete = false;
warningMessage += ", schemaName property missing";
}
if (!propertiesFileIsComplete) {
LOGGER.warn("propertiesFile " + propertiesFileName + " is incomplete" + warningMessage);
LOGGER.warn("Using default Datasource for Test");
schemaName = "TASKANA";
}
} catch (FileNotFoundException e) {
LOGGER.warn("getSchemaNameFromPropertiesObject caught Exception " + e);
LOGGER.warn("Using default schemaName for Test");
} catch (IOException e) {
LOGGER.warn("createDataSourceFromProperties caught Exception " + e);
LOGGER.warn("Using default Datasource for Test");
}
return schemaName;
}
/**
* create Default Datasource for in-memory database.
*
* @return the default datasource.
*/
private static DataSource createDefaultDataSource() {
// JdbcDataSource ds = new JdbcDataSource();
// ds.setURL("jdbc:h2:mem:taskana;IGNORECASE=TRUE;LOCK_MODE=0");
// ds.setPassword("sa");
// ds.setUser("sa");
String jdbcDriver = "org.h2.Driver";
String jdbcUrl =
"jdbc:h2:mem:taskana;IGNORECASE=TRUE;LOCK_MODE=0;"
+ "INIT=CREATE SCHEMA IF NOT EXISTS TASKANA\\;"
+ "SET COLLATION DEFAULT_de_DE ";
String dbUserName = "sa";
String dbPassword = "sa";
PooledDataSource ds =
new PooledDataSource(
Thread.currentThread().getContextClassLoader(),
jdbcDriver,
jdbcUrl,
dbUserName,
dbPassword);
ds.setPoolTimeToWait(POOL_TIME_TO_WAIT);
ds.forceCloseAll(); // otherwise the MyBatis pool is not initialized correctly
return ds;
}
}

View File

@ -0,0 +1,16 @@
package pro.taskana.common.internal.util;
import java.io.File;
public class FileLoaderUtil {
public static boolean loadFromClasspath(String fileToLoad) {
boolean loadFromClasspath = true;
File f = new File(fileToLoad);
if (f.exists() && !f.isDirectory()) {
loadFromClasspath = false;
}
return loadFromClasspath;
}
}

View File

@ -40,6 +40,7 @@ import pro.taskana.common.api.exceptions.WrongCustomHolidayFormatException;
import pro.taskana.common.internal.TaskanaEngineImpl; import pro.taskana.common.internal.TaskanaEngineImpl;
import pro.taskana.common.internal.configuration.DB; import pro.taskana.common.internal.configuration.DB;
import pro.taskana.common.internal.util.CheckedFunction; import pro.taskana.common.internal.util.CheckedFunction;
import pro.taskana.common.internal.util.FileLoaderUtil;
import pro.taskana.common.internal.util.Pair; import pro.taskana.common.internal.util.Pair;
/** /**
@ -555,9 +556,14 @@ public class TaskanaEngineConfiguration {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public Properties readPropertiesFromFile() {
return readPropertiesFromFile(this.propertiesFileName);
}
private Properties readPropertiesFromFile(String propertiesFile) { private Properties readPropertiesFromFile(String propertiesFile) {
Properties props = new Properties(); Properties props = new Properties();
boolean loadFromClasspath = loadFromClasspath(propertiesFile); boolean loadFromClasspath = FileLoaderUtil.loadFromClasspath(propertiesFile);
try { try {
if (loadFromClasspath) { if (loadFromClasspath) {
InputStream inputStream = InputStream inputStream =

View File

@ -1,6 +1,7 @@
package pro.taskana.common.api; package pro.taskana.common.api;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.function.Supplier;
import pro.taskana.TaskanaEngineConfiguration; import pro.taskana.TaskanaEngineConfiguration;
import pro.taskana.classification.api.ClassificationService; import pro.taskana.classification.api.ClassificationService;
@ -115,6 +116,17 @@ public interface TaskanaEngine {
*/ */
void checkRoleMembership(TaskanaRole... roles) throws NotAuthorizedException; void checkRoleMembership(TaskanaRole... roles) throws NotAuthorizedException;
/**
* This method is supposed to skip further permission checks if we are already in a secured
* environment. With great power comes great responsibility.
*
* @param supplier will be executed with admin privileges
* @param <T> defined with the supplier return value
* @return output from supplier
*/
<T> T runAsAdmin(Supplier<T> supplier);
/** /**
* Returns the CurrentUserContext class. * Returns the CurrentUserContext class.
* *

View File

@ -84,13 +84,4 @@ public interface InternalTaskanaEngine {
*/ */
CreateTaskPreprocessorManager getCreateTaskPreprocessorManager(); CreateTaskPreprocessorManager getCreateTaskPreprocessorManager();
/**
* This method is supposed to skip further permission checks if we are already in a secured
* environment. With great power comes great responsibility.
*
* @param supplier will be executed with admin privileges
* @param <T> defined with the supplier return value
* @return output from supplier
*/
<T> T runAsAdmin(Supplier<T> supplier);
} }

View File

@ -1,7 +1,5 @@
package pro.taskana.common.internal; package pro.taskana.common.internal;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction; import java.security.PrivilegedAction;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
@ -42,7 +40,7 @@ import pro.taskana.common.api.exceptions.NotAuthorizedException;
import pro.taskana.common.api.exceptions.SystemException; import pro.taskana.common.api.exceptions.SystemException;
import pro.taskana.common.api.exceptions.TaskanaRuntimeException; import pro.taskana.common.api.exceptions.TaskanaRuntimeException;
import pro.taskana.common.api.security.CurrentUserContext; import pro.taskana.common.api.security.CurrentUserContext;
import pro.taskana.common.api.security.GroupPrincipal; import pro.taskana.common.api.security.UserPrincipal;
import pro.taskana.common.internal.configuration.DB; import pro.taskana.common.internal.configuration.DB;
import pro.taskana.common.internal.configuration.DbSchemaCreator; import pro.taskana.common.internal.configuration.DbSchemaCreator;
import pro.taskana.common.internal.configuration.SecurityVerifier; import pro.taskana.common.internal.configuration.SecurityVerifier;
@ -252,6 +250,19 @@ public class TaskanaEngineImpl implements TaskanaEngine {
return currentUserContext; return currentUserContext;
} }
public <T> T runAsAdmin(Supplier<T> supplier) {
String adminName =
this.getConfiguration().getRoleMap().get(TaskanaRole.ADMIN).stream()
.findFirst()
.orElseThrow(() -> new TaskanaRuntimeException("There is no admin configured"));
Subject subject = new Subject();
subject.getPrincipals().add(new UserPrincipal(adminName));
return Subject.doAs(subject, (PrivilegedAction<T>) supplier::get);
}
/** /**
* This method creates the sqlSessionManager of myBatis. It integrates all the SQL mappers and * This method creates the sqlSessionManager of myBatis. It integrates all the SQL mappers and
* sets the databaseId attribute. * sets the databaseId attribute.
@ -460,30 +471,5 @@ public class TaskanaEngineImpl implements TaskanaEngine {
public CreateTaskPreprocessorManager getCreateTaskPreprocessorManager() { public CreateTaskPreprocessorManager getCreateTaskPreprocessorManager() {
return createTaskPreprocessorManager; return createTaskPreprocessorManager;
} }
@Override
public <T> T runAsAdmin(Supplier<T> supplier) {
Subject subject = Subject.getSubject(AccessController.getContext());
if (subject == null) {
// dont add authorisation if none is available.
return supplier.get();
}
Set<Principal> principalsCopy = new HashSet<>(subject.getPrincipals());
Set<Object> privateCredentialsCopy = new HashSet<>(subject.getPrivateCredentials());
Set<Object> publicCredentialsCopy = new HashSet<>(subject.getPublicCredentials());
String adminName =
this.getEngine().getConfiguration().getRoleMap().get(TaskanaRole.ADMIN).stream()
.findFirst()
.orElseThrow(() -> new TaskanaRuntimeException("There is no admin configured"));
principalsCopy.add(new GroupPrincipal(adminName));
Subject subject1 =
new Subject(true, principalsCopy, privateCredentialsCopy, publicCredentialsCopy);
return Subject.doAs(subject1, (PrivilegedAction<T>) supplier::get);
}
} }
} }

View File

@ -48,7 +48,7 @@ public class TaskStatusReportBuilderImpl implements TaskStatusReport.Builder {
TaskStatusReport report = new TaskStatusReport(this.states); TaskStatusReport report = new TaskStatusReport(this.states);
report.addItems(tasks); report.addItems(tasks);
Map<String, String> displayMap = Map<String, String> displayMap =
taskanaEngine.runAsAdmin( taskanaEngine.getEngine().runAsAdmin(
() -> () ->
workbasketService.createWorkbasketQuery() workbasketService.createWorkbasketQuery()
.keyIn(report.getRows().keySet().toArray(new String[0])) .keyIn(report.getRows().keySet().toArray(new String[0]))

View File

@ -68,7 +68,7 @@ public class WorkbasketReportBuilderImpl
this.columnHeaders, converter, this.inWorkingDays)); this.columnHeaders, converter, this.inWorkingDays));
Map<String, String> displayMap = Map<String, String> displayMap =
taskanaEngine.runAsAdmin( taskanaEngine.getEngine().runAsAdmin(
() -> () ->
workbasketService workbasketService
.createWorkbasketQuery() .createWorkbasketQuery()

View File

@ -902,7 +902,7 @@ public class TaskServiceImpl implements TaskService {
serviceLevelHandler.refreshPriorityAndDueDatesOfTasks( serviceLevelHandler.refreshPriorityAndDueDatesOfTasks(
tasks, serviceLevelChanged, priorityChanged); tasks, serviceLevelChanged, priorityChanged);
} else { } else {
taskanaEngine.runAsAdmin( taskanaEngine.getEngine().runAsAdmin(
() -> { () -> {
serviceLevelHandler.refreshPriorityAndDueDatesOfTasks( serviceLevelHandler.refreshPriorityAndDueDatesOfTasks(
tasks, serviceLevelChanged, priorityChanged); tasks, serviceLevelChanged, priorityChanged);

View File

@ -845,7 +845,9 @@ public class WorkbasketServiceImpl implements WorkbasketService {
} }
long countTasksNotCompletedInWorkbasket = long countTasksNotCompletedInWorkbasket =
taskanaEngine.runAsAdmin(() -> getCountTasksNotCompletedByWorkbasketId(workbasketId)); taskanaEngine
.getEngine()
.runAsAdmin(() -> getCountTasksNotCompletedByWorkbasketId(workbasketId));
if (countTasksNotCompletedInWorkbasket > 0) { if (countTasksNotCompletedInWorkbasket > 0) {
String errorMessage = String errorMessage =
@ -856,7 +858,7 @@ public class WorkbasketServiceImpl implements WorkbasketService {
} }
long countTasksInWorkbasket = long countTasksInWorkbasket =
taskanaEngine.runAsAdmin(() -> getCountTasksByWorkbasketId(workbasketId)); taskanaEngine.getEngine().runAsAdmin(() -> getCountTasksByWorkbasketId(workbasketId));
boolean canBeDeletedNow = countTasksInWorkbasket == 0; boolean canBeDeletedNow = countTasksInWorkbasket == 0;

View File

@ -156,7 +156,7 @@ class SetOwnerAccTest extends AbstractAccTest {
resetDb(false); resetDb(false);
List<TaskSummary> allTaskSummaries = List<TaskSummary> allTaskSummaries =
new TaskanaEngineProxy(taskanaEngine) new TaskanaEngineProxy(taskanaEngine)
.getEngine() .getEngine().getEngine()
.runAsAdmin(() -> taskanaEngine.getTaskService().createTaskQuery().list()); .runAsAdmin(() -> taskanaEngine.getTaskService().createTaskQuery().list());
List<String> allTaskIds = List<String> allTaskIds =
allTaskSummaries.stream().map(TaskSummary::getId).collect(Collectors.toList()); allTaskSummaries.stream().map(TaskSummary::getId).collect(Collectors.toList());

View File

@ -37,7 +37,7 @@ class TaskEngineAccTest extends AbstractAccTest {
assertThat(taskanaEngine.isUserInRole(TaskanaRole.ADMIN)).isFalse(); assertThat(taskanaEngine.isUserInRole(TaskanaRole.ADMIN)).isFalse();
new TaskanaEngineProxy(taskanaEngine) new TaskanaEngineProxy(taskanaEngine)
.getEngine() .getEngine().getEngine()
.runAsAdmin(() -> assertThat(taskanaEngine.isUserInRole(TaskanaRole.ADMIN)).isTrue()); .runAsAdmin(() -> assertThat(taskanaEngine.isUserInRole(TaskanaRole.ADMIN)).isTrue());
assertThat(taskanaEngine.isUserInRole(TaskanaRole.ADMIN)).isFalse(); assertThat(taskanaEngine.isUserInRole(TaskanaRole.ADMIN)).isFalse();

View File

@ -18,6 +18,7 @@
<!-- History is an optional module. --> <!-- History is an optional module. -->
<module>history</module> <module>history</module>
<module>ci/taskana-sonar-test-coverage</module> <module>ci/taskana-sonar-test-coverage</module>
<module>taskana-routing-parent</module>
</modules> </modules>
<properties> <properties>
@ -58,6 +59,10 @@
<!-- wildfly dependencies --> <!-- wildfly dependencies -->
<version.wildfly>13.0.0.Final</version.wildfly> <version.wildfly>13.0.0.Final</version.wildfly>
<!-- camunda dependencies -->
<version.camunda.dmn>7.14.0</version.camunda.dmn>
<!-- java ee dependencies --> <!-- java ee dependencies -->
<version.resteasy>4.6.0.Final</version.resteasy> <version.resteasy>4.6.0.Final</version.resteasy>
<version.thorntail>2.7.0.Final</version.thorntail> <version.thorntail>2.7.0.Final</version.thorntail>

View File

@ -10,6 +10,9 @@ import org.springframework.context.annotation.DependsOn;
import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import pro.taskana.TaskanaEngineConfiguration;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.internal.configuration.DbSchemaCreator;
import pro.taskana.sampledata.SampleDataGenerator; import pro.taskana.sampledata.SampleDataGenerator;
@Configuration @Configuration
@ -21,18 +24,30 @@ public class ExampleRestConfiguration {
} }
@Bean @Bean
@DependsOn("getTaskanaEngine") // generate sample data after schema was inserted @DependsOn("taskanaEngineConfiguration") // generate sample data after schema was inserted
public SampleDataGenerator generateSampleData( public SampleDataGenerator generateSampleData(
TaskanaEngineConfiguration taskanaEngineConfiguration,
DataSource dataSource, DataSource dataSource,
@Value("${taskana.schemaName:TASKANA}") String schemaName, @Value("${generateSampleData:true}") boolean generateSampleData)
@Value("${generateSampleData:true}") boolean generateSampleData) { throws SQLException {
SampleDataGenerator sampleDataGenerator = new SampleDataGenerator(dataSource, schemaName); DbSchemaCreator dbSchemaCreator =
new DbSchemaCreator(dataSource, taskanaEngineConfiguration.getSchemaName());
dbSchemaCreator.run();
SampleDataGenerator sampleDataGenerator =
new SampleDataGenerator(dataSource, taskanaEngineConfiguration.getSchemaName());
if (generateSampleData) { if (generateSampleData) {
sampleDataGenerator.generateSampleData(); sampleDataGenerator.generateSampleData();
} }
return sampleDataGenerator; return sampleDataGenerator;
} }
@Bean
@DependsOn("generateSampleData")
public TaskanaEngine getTaskanaEngine(TaskanaEngineConfiguration taskanaEngineConfiguration)
throws SQLException {
return taskanaEngineConfiguration.buildTaskanaEngine();
}
// only required to let the adapter example connect to the same database // only required to let the adapter example connect to the same database
@Bean(initMethod = "start", destroyMethod = "stop") @Bean(initMethod = "start", destroyMethod = "stop")
public Server inMemoryH2DatabaseaServer() throws SQLException { public Server inMemoryH2DatabaseaServer() throws SQLException {

View File

@ -53,6 +53,7 @@ public class RestConfiguration {
} }
@Bean @Bean
@ConditionalOnMissingBean(TaskanaEngine.class)
public TaskanaEngine getTaskanaEngine(TaskanaEngineConfiguration taskanaEngineConfiguration) public TaskanaEngine getTaskanaEngine(TaskanaEngineConfiguration taskanaEngineConfiguration)
throws SQLException { throws SQLException {
return taskanaEngineConfiguration.buildTaskanaEngine(); return taskanaEngineConfiguration.buildTaskanaEngine();

View File

@ -0,0 +1,27 @@
<?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>taskana-routing-parent</artifactId>
<packaging>pom</packaging>
<name>${project.groupId}:${project.artifactId}</name>
<description>This pom is parent to all taskana routing modules</description>
<parent>
<groupId>pro.taskana</groupId>
<artifactId>taskana-parent</artifactId>
<version>4.5.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modules>
<module>taskana-spi-routing-dmn-router</module>
</modules>
</project>

View File

@ -0,0 +1,63 @@
<?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">
<parent>
<artifactId>taskana-routing-parent</artifactId>
<groupId>pro.taskana</groupId>
<version>4.5.2-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>taskana-spi-routing-dmn-router</artifactId>
<dependencies>
<dependency>
<groupId>pro.taskana</groupId>
<artifactId>taskana-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.camunda.bpm.model</groupId>
<artifactId>camunda-dmn-model</artifactId>
<version>${version.camunda.dmn}</version>
</dependency>
<dependency>
<groupId>org.camunda.bpm.dmn</groupId>
<artifactId>camunda-engine-dmn</artifactId>
<version>${version.camunda.dmn}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- test dependencies start -->
<dependency>
<groupId>pro.taskana</groupId>
<artifactId>taskana-common-data</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>pro.taskana</groupId>
<artifactId>taskana-common-test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- test dependencies end -->
</project>

View File

@ -0,0 +1,211 @@
package pro.taskana.routing.dmn;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.camunda.bpm.dmn.engine.DmnDecision;
import org.camunda.bpm.dmn.engine.DmnDecisionTableResult;
import org.camunda.bpm.dmn.engine.DmnEngine;
import org.camunda.bpm.dmn.engine.DmnEngineConfiguration;
import org.camunda.bpm.engine.variable.VariableMap;
import org.camunda.bpm.engine.variable.Variables;
import org.camunda.bpm.model.dmn.Dmn;
import org.camunda.bpm.model.dmn.DmnModelInstance;
import org.camunda.bpm.model.dmn.instance.OutputEntry;
import org.camunda.bpm.model.dmn.instance.Rule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.exceptions.NotAuthorizedException;
import pro.taskana.common.api.exceptions.SystemException;
import pro.taskana.common.internal.util.FileLoaderUtil;
import pro.taskana.common.internal.util.Pair;
import pro.taskana.spi.routing.api.TaskRoutingProvider;
import pro.taskana.task.api.models.Task;
import pro.taskana.workbasket.api.WorkbasketService;
import pro.taskana.workbasket.api.exceptions.WorkbasketNotFoundException;
public class DmnTaskRouter implements TaskRoutingProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(DmnTaskRouter.class);
private static final String DECISION_ID = "workbasketRouting";
private static final String DECISION_VARIABLE_MAP_NAME = "task";
private static final String OUTPUT_WORKBASKET_KEY = "workbasketKey";
private static final String OUTPUT_DOMAIN = "domain";
private static final String DMN_TABLE_PROPERTY = "taskana.routing.dmn";
private TaskanaEngine taskanaEngine;
private DmnEngine dmnEngine;
private DmnDecision decision;
@Override
public void initialize(TaskanaEngine taskanaEngine) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entering initialize()");
}
this.taskanaEngine = taskanaEngine;
dmnEngine = DmnEngineConfiguration.createDefaultDmnEngineConfiguration().buildEngine();
DmnModelInstance dmnModel = readModelFromDmnTable();
decision = dmnEngine.parseDecision(DECISION_ID, dmnModel);
validateOutputs(dmnModel);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exiting initialize()");
}
}
@Override
public String determineWorkbasketId(Task task) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entering determineWorkbasketId(task = {})", task);
}
VariableMap variables = Variables.putValue(DECISION_VARIABLE_MAP_NAME, task);
DmnDecisionTableResult result = dmnEngine.evaluateDecisionTable(decision, variables);
if (result.getSingleResult() == null) {
return null;
}
String workbasketKey = result.getSingleResult().getEntry(OUTPUT_WORKBASKET_KEY);
String domain = result.getSingleResult().getEntry(OUTPUT_DOMAIN);
try {
String determinedWorkbasketId =
taskanaEngine.getWorkbasketService().getWorkbasket(workbasketKey, domain).getId();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
String.format("Exiting determineWorkbasketId, returning %s", determinedWorkbasketId));
}
return determinedWorkbasketId;
} catch (WorkbasketNotFoundException e) {
throw new SystemException(
String.format(
"Unknown workbasket defined in DMN Table. key: '%s', domain: '%s'",
workbasketKey, domain));
} catch (NotAuthorizedException e) {
throw new SystemException(
String.format(
"The current user is not authorized to create a task in the routed workbasket. "
+ "key: '%s', domain: '%s'",
workbasketKey, domain));
}
}
protected Set<Pair<String, String>> getAllWorkbasketAndDomainOutputs(DmnModelInstance dmnModel) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entering getAllWorkbasketAndDomainOutputs()");
}
Set<Pair<String, String>> allWorkbasketAndDomainOutputs = new HashSet<>();
for (Rule rule : dmnModel.getModelElementsByType(Rule.class)) {
List<OutputEntry> outputEntries = new ArrayList<>(rule.getOutputEntries());
String workbasketKey = outputEntries.get(0).getTextContent();
String domain = outputEntries.get(1).getTextContent();
allWorkbasketAndDomainOutputs.add(Pair.of(workbasketKey, domain));
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exiting getAllWorkbasketAndDomainOutputs()");
}
return allWorkbasketAndDomainOutputs;
}
protected DmnModelInstance readModelFromDmnTable() {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entering readModelFromDmnTable()");
}
String pathToDmn =
taskanaEngine.getConfiguration().readPropertiesFromFile().getProperty(DMN_TABLE_PROPERTY);
if (FileLoaderUtil.loadFromClasspath(pathToDmn)) {
try (InputStream inputStream = DmnTaskRouter.class.getResourceAsStream(pathToDmn)) {
if (inputStream == null) {
LOGGER.error("dmn file {} was not found on classpath.", pathToDmn);
} else {
return Dmn.readModelFromStream(inputStream);
}
} catch (IOException e) {
LOGGER.error("caught IOException when processing dmn file");
throw new SystemException("Internal System error when processing dmn file", e.getCause());
}
}
try (FileInputStream inputStream = new FileInputStream(pathToDmn)) {
return Dmn.readModelFromStream(inputStream);
} catch (IOException e) {
throw new SystemException(
String.format("Could not find a dmn file with provided path %s", pathToDmn));
}
}
private void validateOutputs(DmnModelInstance dmnModel) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entering validateOutputs()");
}
Set<Pair<String, String>> allWorkbasketAndDomainOutputs =
getAllWorkbasketAndDomainOutputs(dmnModel);
validate(allWorkbasketAndDomainOutputs);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exiting validateOutputs()");
}
}
private void validate(Set<Pair<String, String>> allWorkbasketAndDomainOutputs) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"Entering validate(allWorkbasketAndDomainOutputs = {}", allWorkbasketAndDomainOutputs);
}
WorkbasketService workbasketService = taskanaEngine.getWorkbasketService();
for (Pair<String, String> pair : allWorkbasketAndDomainOutputs) {
String workbasketKey = pair.getLeft().replace("\"", "");
String domain = pair.getRight().replace("\"", "");
// This can be replaced with a workbasketQuery call.
// Unfortunately the WorkbasketQuery does not support a keyDomainIn operation.
// Therefore we fetch every workbasket separately
taskanaEngine.runAsAdmin(
() -> {
try {
return workbasketService.getWorkbasket(workbasketKey, domain);
} catch (Exception e) {
throw new SystemException(
String.format(
"Unknown workbasket defined in DMN Table. key: '%s', domain: '%s'",
workbasketKey, domain),
e);
}
});
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exiting validate()");
}
}
}

View File

@ -0,0 +1,57 @@
package acceptance;
import javax.sql.DataSource;
import org.junit.jupiter.api.BeforeAll;
import pro.taskana.TaskanaEngineConfiguration;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.TaskanaEngine.ConnectionManagementMode;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.internal.configuration.DbSchemaCreator;
import pro.taskana.common.test.config.TaskanaEngineTestConfiguration;
import pro.taskana.sampledata.SampleDataGenerator;
import pro.taskana.task.api.models.ObjectReference;
public abstract class AbstractAccTest {
protected static TaskanaEngineConfiguration taskanaEngineConfiguration;
protected static TaskanaEngine taskanaEngine;
protected static WorkingDaysToDaysConverter converter;
@BeforeAll
protected static void setupTest() throws Exception {
resetDb(false);
}
protected static void resetDb(boolean dropTables) throws Exception {
DataSource dataSource = TaskanaEngineTestConfiguration.getDataSource();
String schemaName = TaskanaEngineTestConfiguration.getSchemaName();
SampleDataGenerator sampleDataGenerator = new SampleDataGenerator(dataSource, schemaName);
if (dropTables) {
sampleDataGenerator.dropDb();
}
dataSource = TaskanaEngineTestConfiguration.getDataSource();
taskanaEngineConfiguration = new TaskanaEngineConfiguration(dataSource, false, schemaName);
taskanaEngineConfiguration.setGermanPublicHolidaysEnabled(true);
DbSchemaCreator dbSchemaCreator =
new DbSchemaCreator(dataSource, taskanaEngineConfiguration.getSchemaName());
dbSchemaCreator.run();
sampleDataGenerator.clearDb();
sampleDataGenerator.generateTestData();
taskanaEngine = taskanaEngineConfiguration.buildTaskanaEngine();
taskanaEngine.setConnectionManagementMode(ConnectionManagementMode.AUTOCOMMIT);
converter = taskanaEngine.getWorkingDaysToDaysConverter();
}
protected ObjectReference createObjectReference(
String company, String system, String systemInstance, String type, String value) {
ObjectReference objectReference = new ObjectReference();
objectReference.setCompany(company);
objectReference.setSystem(system);
objectReference.setSystemInstance(systemInstance);
objectReference.setType(type);
objectReference.setValue(value);
return objectReference;
}
}

View File

@ -0,0 +1,53 @@
package pro.taskana.routing.dmn;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import acceptance.AbstractAccTest;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.test.security.JaasExtension;
import pro.taskana.common.test.security.WithAccessId;
import pro.taskana.task.api.TaskService;
import pro.taskana.task.api.models.ObjectReference;
import pro.taskana.task.api.models.Task;
@ExtendWith(JaasExtension.class)
public class DmnTaskRouterTest extends AbstractAccTest {
private final TaskService taskService = taskanaEngine.getTaskService();
@WithAccessId(user = "taskadmin")
@Test
void should_RouteTaskToCorrectWorkbasket_When_DmnTaskRouterFindsRule() throws Exception {
Task taskToRoute = taskService.newTask();
taskToRoute.setClassificationKey("T2100");
ObjectReference objectReference =
createObjectReference("company", null, null, "MyType1", "00000001");
taskToRoute.setPrimaryObjRef(objectReference);
Task routedTask = taskService.createTask(taskToRoute);
assertThat(routedTask.getWorkbasketKey()).isEqualTo("GPK_KSC");
}
@WithAccessId(user = "taskadmin")
@Test
void should_ThrowException_When_DmnTaskRouterFindsNoRule() throws Exception {
Task taskToRoute = taskService.newTask();
taskToRoute.setClassificationKey("T2100");
ObjectReference objectReference =
createObjectReference("company", null, null, "MyTeö", "000002");
taskToRoute.setPrimaryObjRef(objectReference);
ThrowingCallable call = () -> taskService.createTask(taskToRoute);
assertThatThrownBy(call)
.isInstanceOf(InvalidArgumentException.class)
.extracting(ex -> ex.getMessage())
.isEqualTo("Cannot create a task outside a workbasket");
}
}

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns:ns0="http://camunda.org/schema/1.0/dmn" xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/1.0" id="definitions" name="definitions" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.3.5">
<decision id="workbasketRouting" name="Workbasket Routing">
<extensionElements>
<biodi:bounds x="150" y="150" width="180" height="80" />
</extensionElements>
<decisionTable id="workbasketRouting_decisionTable" hitPolicy="FIRST">
<input id="workbasketRouting_decisionTable-input_2" label="porValue" ns0:inputVariable="input">
<inputExpression id="workbasketRouting_decisionTable-inputExpression_2" typeRef="string">
<text>task.primaryObjRef.value</text>
</inputExpression>
</input>
<input id="InputClause_1qkek3h" label="porType" ns0:inputVariable="input">
<inputExpression id="LiteralExpression_1k84ufl" typeRef="string">
<text>task.primaryObjRef.type</text>
</inputExpression>
</input>
<output id="workbasketRouting_decisionTable-output_workbasketKey" label="Workbasket key" name="workbasketKey" typeRef="string" />
<output id="workbasketRouting_decisionTable-output_domain" label="Domain" name="domain" typeRef="string" />
<rule id="workbasketRouting_decisionTable-rule_0">
<inputEntry id="workbasketRouting_decisionTable-rule_0-inputEntry_2">
<text>"00000001"</text>
</inputEntry>
<inputEntry id="UnaryTests_0n5ac31">
<text>"MyType1"</text>
</inputEntry>
<outputEntry id="workbasketRouting_decisionTable-rule_0-outputEntry_workbasketKey">
<text>"GPK_KSC"</text>
</outputEntry>
<outputEntry id="workbasketRouting_decisionTable-rule_0-outputEntry_domain">
<text>"DOMAIN_A"</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0ua6ja6">
<inputEntry id="UnaryTests_1kdwhvv">
<text>"00000001"</text>
</inputEntry>
<inputEntry id="UnaryTests_03hbryc">
<text></text>
</inputEntry>
<outputEntry id="workbasketRouting_decisionTable-rule_1-outputEntry_workbasketKey">
<text>"GPK_KSC_1"</text>
</outputEntry>
<outputEntry id="workbasketRouting_decisionTable-rule_1-outputEntry_domain">
<text>"DOMAIN_A"</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0elr8ov">
<inputEntry id="UnaryTests_0n7qv3k">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0jwi2ra">
<text>"MyType1"</text>
</inputEntry>
<outputEntry id="workbasketRouting_decisionTable-rule_2-outputEntry_workbasketKey">
<text>"GPK_KSC_2"</text>
</outputEntry>
<outputEntry id="workbasketRouting_decisionTable-rule_2-outputEntry_domain">
<text>"DOMAIN_A"</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@ -0,0 +1,25 @@
taskana.roles.user=teamlead-1 | teamlead-2 | user-1-1 | user-1-2 | user-2-1 | user-2-2 | user-b-1 | user-b-2
taskana.roles.admin=admin | uid=admin,cn=users,OU=Test,O=TASKANA
taskana.roles.businessadmin=businessadmin | cn=business-admins,cn=groups,OU=Test,O=TASKANA
taskana.roles.monitor=monitor | cn=monitor-users,cn=groups,OU=Test,O=TASKANA
taskana.roles.taskadmin=taskadmin
taskana.domains=DOMAIN_A,DOMAIN_B,DOMAIN_C
taskana.classification.types=TASK,DOCUMENT
taskana.classification.categories.task= EXTERNAL, manual, autoMAtic, Process
taskana.classification.categories.document= EXTERNAL
taskana.jobs.maxRetries=3
taskana.jobs.batchSize=50
taskana.jobs.cleanup.runEvery=P1D
taskana.jobs.cleanup.firstRunAt=2018-07-25T08:00:00Z
taskana.jobs.cleanup.minimumAge=P14D
taskana.jobs.history.batchSize=50
taskana.jobs.history.cleanup.firstRunAt=2018-07-25T08:00:00Z
taskana.jobs.history.cleanup.minimumAge=P14D
taskana.jobs.history.cleanup.runEvery=P1D
taskana.german.holidays.enabled=true
taskana.german.holidays.corpus-christi.enabled=true
taskana.historylogger.name=AUDIT
taskana.routing.dmn=/dmn-table.dmn