From 3ddcd2ae97ffea8bd479655e70d5182141d12220 Mon Sep 17 00:00:00 2001 From: Joerg Heffner <56156750+gitgoodjhe@users.noreply.github.com> Date: Wed, 14 Apr 2021 15:42:23 +0200 Subject: [PATCH] TSK-1628: New DmnTaskRouter module --- .../TaskanaEngineTestConfiguration.java | 194 ++++++++++++++++ .../common/internal/util/FileLoaderUtil.java | 16 ++ .../taskana/TaskanaEngineConfiguration.java | 8 +- .../pro/taskana/common/api/TaskanaEngine.java | 12 + .../internal/InternalTaskanaEngine.java | 9 - .../common/internal/TaskanaEngineImpl.java | 42 ++-- .../reports/TaskStatusReportBuilderImpl.java | 2 +- .../reports/WorkbasketReportBuilderImpl.java | 2 +- .../task/internal/TaskServiceImpl.java | 2 +- .../internal/WorkbasketServiceImpl.java | 6 +- .../java/acceptance/task/SetOwnerAccTest.java | 2 +- .../acceptance/task/TaskEngineAccTest.java | 2 +- pom.xml | 5 + .../boot/ExampleRestConfiguration.java | 23 +- .../common/rest/RestConfiguration.java | 1 + taskana-routing-parent/pom.xml | 27 +++ .../taskana-spi-routing-dmn-router/pom.xml | 63 ++++++ .../taskana/routing/dmn/DmnTaskRouter.java | 211 ++++++++++++++++++ .../test/java/acceptance/AbstractAccTest.java | 57 +++++ .../routing/dmn/DmnTaskRouterTest.java | 53 +++++ ...askana.spi.routing.api.TaskRoutingProvider | 1 + .../src/test/resources/dmn-table.dmn | 64 ++++++ .../src/test/resources/taskana.properties | 25 +++ 23 files changed, 778 insertions(+), 49 deletions(-) create mode 100644 common/taskana-common-test/src/main/java/pro/taskana/common/test/config/TaskanaEngineTestConfiguration.java create mode 100644 common/taskana-common/src/main/java/pro/taskana/common/internal/util/FileLoaderUtil.java create mode 100644 taskana-routing-parent/pom.xml create mode 100644 taskana-routing-parent/taskana-spi-routing-dmn-router/pom.xml create mode 100644 taskana-routing-parent/taskana-spi-routing-dmn-router/src/main/java/pro/taskana/routing/dmn/DmnTaskRouter.java create mode 100644 taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/java/acceptance/AbstractAccTest.java create mode 100644 taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/java/pro/taskana/routing/dmn/DmnTaskRouterTest.java create mode 100644 taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/resources/META-INF/services/pro.taskana.spi.routing.api.TaskRoutingProvider create mode 100644 taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/resources/dmn-table.dmn create mode 100644 taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/resources/taskana.properties diff --git a/common/taskana-common-test/src/main/java/pro/taskana/common/test/config/TaskanaEngineTestConfiguration.java b/common/taskana-common-test/src/main/java/pro/taskana/common/test/config/TaskanaEngineTestConfiguration.java new file mode 100644 index 000000000..acbb404e1 --- /dev/null +++ b/common/taskana-common-test/src/main/java/pro/taskana/common/test/config/TaskanaEngineTestConfiguration.java @@ -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; + } +} diff --git a/common/taskana-common/src/main/java/pro/taskana/common/internal/util/FileLoaderUtil.java b/common/taskana-common/src/main/java/pro/taskana/common/internal/util/FileLoaderUtil.java new file mode 100644 index 000000000..4df82b9fa --- /dev/null +++ b/common/taskana-common/src/main/java/pro/taskana/common/internal/util/FileLoaderUtil.java @@ -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; + } + +} diff --git a/lib/taskana-core/src/main/java/pro/taskana/TaskanaEngineConfiguration.java b/lib/taskana-core/src/main/java/pro/taskana/TaskanaEngineConfiguration.java index 1d0e3ac06..e46a7e22c 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/TaskanaEngineConfiguration.java +++ b/lib/taskana-core/src/main/java/pro/taskana/TaskanaEngineConfiguration.java @@ -40,6 +40,7 @@ import pro.taskana.common.api.exceptions.WrongCustomHolidayFormatException; import pro.taskana.common.internal.TaskanaEngineImpl; import pro.taskana.common.internal.configuration.DB; import pro.taskana.common.internal.util.CheckedFunction; +import pro.taskana.common.internal.util.FileLoaderUtil; import pro.taskana.common.internal.util.Pair; /** @@ -555,9 +556,14 @@ public class TaskanaEngineConfiguration { .collect(Collectors.toList()); } + public Properties readPropertiesFromFile() { + return readPropertiesFromFile(this.propertiesFileName); + } + + private Properties readPropertiesFromFile(String propertiesFile) { Properties props = new Properties(); - boolean loadFromClasspath = loadFromClasspath(propertiesFile); + boolean loadFromClasspath = FileLoaderUtil.loadFromClasspath(propertiesFile); try { if (loadFromClasspath) { InputStream inputStream = diff --git a/lib/taskana-core/src/main/java/pro/taskana/common/api/TaskanaEngine.java b/lib/taskana-core/src/main/java/pro/taskana/common/api/TaskanaEngine.java index 44de7ee0a..b2550e6c3 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/common/api/TaskanaEngine.java +++ b/lib/taskana-core/src/main/java/pro/taskana/common/api/TaskanaEngine.java @@ -1,6 +1,7 @@ package pro.taskana.common.api; import java.sql.SQLException; +import java.util.function.Supplier; import pro.taskana.TaskanaEngineConfiguration; import pro.taskana.classification.api.ClassificationService; @@ -115,6 +116,17 @@ public interface TaskanaEngine { */ 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 defined with the supplier return value + * @return output from supplier + */ + T runAsAdmin(Supplier supplier); + + /** * Returns the CurrentUserContext class. * diff --git a/lib/taskana-core/src/main/java/pro/taskana/common/internal/InternalTaskanaEngine.java b/lib/taskana-core/src/main/java/pro/taskana/common/internal/InternalTaskanaEngine.java index 5f65344bf..e639e49e9 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/common/internal/InternalTaskanaEngine.java +++ b/lib/taskana-core/src/main/java/pro/taskana/common/internal/InternalTaskanaEngine.java @@ -84,13 +84,4 @@ public interface InternalTaskanaEngine { */ 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 defined with the supplier return value - * @return output from supplier - */ - T runAsAdmin(Supplier supplier); } diff --git a/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java b/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java index b8fbc1207..c5bf05cab 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java @@ -1,7 +1,5 @@ package pro.taskana.common.internal; -import java.security.AccessController; -import java.security.Principal; import java.security.PrivilegedAction; import java.sql.Connection; 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.TaskanaRuntimeException; 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.DbSchemaCreator; import pro.taskana.common.internal.configuration.SecurityVerifier; @@ -252,6 +250,19 @@ public class TaskanaEngineImpl implements TaskanaEngine { return currentUserContext; } + public T runAsAdmin(Supplier 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) supplier::get); + } + /** * This method creates the sqlSessionManager of myBatis. It integrates all the SQL mappers and * sets the databaseId attribute. @@ -460,30 +471,5 @@ public class TaskanaEngineImpl implements TaskanaEngine { public CreateTaskPreprocessorManager getCreateTaskPreprocessorManager() { return createTaskPreprocessorManager; } - - @Override - public T runAsAdmin(Supplier supplier) { - - Subject subject = Subject.getSubject(AccessController.getContext()); - if (subject == null) { - // dont add authorisation if none is available. - return supplier.get(); - } - - Set principalsCopy = new HashSet<>(subject.getPrincipals()); - Set privateCredentialsCopy = new HashSet<>(subject.getPrivateCredentials()); - Set 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) supplier::get); - } } } diff --git a/lib/taskana-core/src/main/java/pro/taskana/monitor/internal/reports/TaskStatusReportBuilderImpl.java b/lib/taskana-core/src/main/java/pro/taskana/monitor/internal/reports/TaskStatusReportBuilderImpl.java index f3ffe6942..2742ff05b 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/monitor/internal/reports/TaskStatusReportBuilderImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/monitor/internal/reports/TaskStatusReportBuilderImpl.java @@ -48,7 +48,7 @@ public class TaskStatusReportBuilderImpl implements TaskStatusReport.Builder { TaskStatusReport report = new TaskStatusReport(this.states); report.addItems(tasks); Map displayMap = - taskanaEngine.runAsAdmin( + taskanaEngine.getEngine().runAsAdmin( () -> workbasketService.createWorkbasketQuery() .keyIn(report.getRows().keySet().toArray(new String[0])) diff --git a/lib/taskana-core/src/main/java/pro/taskana/monitor/internal/reports/WorkbasketReportBuilderImpl.java b/lib/taskana-core/src/main/java/pro/taskana/monitor/internal/reports/WorkbasketReportBuilderImpl.java index ac932ec31..6173a00b7 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/monitor/internal/reports/WorkbasketReportBuilderImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/monitor/internal/reports/WorkbasketReportBuilderImpl.java @@ -68,7 +68,7 @@ public class WorkbasketReportBuilderImpl this.columnHeaders, converter, this.inWorkingDays)); Map displayMap = - taskanaEngine.runAsAdmin( + taskanaEngine.getEngine().runAsAdmin( () -> workbasketService .createWorkbasketQuery() diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskServiceImpl.java b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskServiceImpl.java index dc29c027b..c77d556a7 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskServiceImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskServiceImpl.java @@ -902,7 +902,7 @@ public class TaskServiceImpl implements TaskService { serviceLevelHandler.refreshPriorityAndDueDatesOfTasks( tasks, serviceLevelChanged, priorityChanged); } else { - taskanaEngine.runAsAdmin( + taskanaEngine.getEngine().runAsAdmin( () -> { serviceLevelHandler.refreshPriorityAndDueDatesOfTasks( tasks, serviceLevelChanged, priorityChanged); diff --git a/lib/taskana-core/src/main/java/pro/taskana/workbasket/internal/WorkbasketServiceImpl.java b/lib/taskana-core/src/main/java/pro/taskana/workbasket/internal/WorkbasketServiceImpl.java index aef141bfb..4080fd495 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/workbasket/internal/WorkbasketServiceImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/workbasket/internal/WorkbasketServiceImpl.java @@ -845,7 +845,9 @@ public class WorkbasketServiceImpl implements WorkbasketService { } long countTasksNotCompletedInWorkbasket = - taskanaEngine.runAsAdmin(() -> getCountTasksNotCompletedByWorkbasketId(workbasketId)); + taskanaEngine + .getEngine() + .runAsAdmin(() -> getCountTasksNotCompletedByWorkbasketId(workbasketId)); if (countTasksNotCompletedInWorkbasket > 0) { String errorMessage = @@ -856,7 +858,7 @@ public class WorkbasketServiceImpl implements WorkbasketService { } long countTasksInWorkbasket = - taskanaEngine.runAsAdmin(() -> getCountTasksByWorkbasketId(workbasketId)); + taskanaEngine.getEngine().runAsAdmin(() -> getCountTasksByWorkbasketId(workbasketId)); boolean canBeDeletedNow = countTasksInWorkbasket == 0; diff --git a/lib/taskana-core/src/test/java/acceptance/task/SetOwnerAccTest.java b/lib/taskana-core/src/test/java/acceptance/task/SetOwnerAccTest.java index 8095140db..b56fb2dde 100644 --- a/lib/taskana-core/src/test/java/acceptance/task/SetOwnerAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/task/SetOwnerAccTest.java @@ -156,7 +156,7 @@ class SetOwnerAccTest extends AbstractAccTest { resetDb(false); List allTaskSummaries = new TaskanaEngineProxy(taskanaEngine) - .getEngine() + .getEngine().getEngine() .runAsAdmin(() -> taskanaEngine.getTaskService().createTaskQuery().list()); List allTaskIds = allTaskSummaries.stream().map(TaskSummary::getId).collect(Collectors.toList()); diff --git a/lib/taskana-core/src/test/java/acceptance/task/TaskEngineAccTest.java b/lib/taskana-core/src/test/java/acceptance/task/TaskEngineAccTest.java index 7724b1501..6e3506e5f 100644 --- a/lib/taskana-core/src/test/java/acceptance/task/TaskEngineAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/task/TaskEngineAccTest.java @@ -37,7 +37,7 @@ class TaskEngineAccTest extends AbstractAccTest { assertThat(taskanaEngine.isUserInRole(TaskanaRole.ADMIN)).isFalse(); new TaskanaEngineProxy(taskanaEngine) - .getEngine() + .getEngine().getEngine() .runAsAdmin(() -> assertThat(taskanaEngine.isUserInRole(TaskanaRole.ADMIN)).isTrue()); assertThat(taskanaEngine.isUserInRole(TaskanaRole.ADMIN)).isFalse(); diff --git a/pom.xml b/pom.xml index 3395c7274..1045dd1f6 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,7 @@ history ci/taskana-sonar-test-coverage + taskana-routing-parent @@ -58,6 +59,10 @@ 13.0.0.Final + + 7.14.0 + + 4.6.0.Final 2.7.0.Final diff --git a/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/ExampleRestConfiguration.java b/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/ExampleRestConfiguration.java index aa86e7514..ccb2dc6be 100644 --- a/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/ExampleRestConfiguration.java +++ b/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/ExampleRestConfiguration.java @@ -10,6 +10,9 @@ import org.springframework.context.annotation.DependsOn; import org.springframework.jdbc.datasource.DataSourceTransactionManager; 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; @Configuration @@ -21,18 +24,30 @@ public class ExampleRestConfiguration { } @Bean - @DependsOn("getTaskanaEngine") // generate sample data after schema was inserted + @DependsOn("taskanaEngineConfiguration") // generate sample data after schema was inserted public SampleDataGenerator generateSampleData( + TaskanaEngineConfiguration taskanaEngineConfiguration, DataSource dataSource, - @Value("${taskana.schemaName:TASKANA}") String schemaName, - @Value("${generateSampleData:true}") boolean generateSampleData) { - SampleDataGenerator sampleDataGenerator = new SampleDataGenerator(dataSource, schemaName); + @Value("${generateSampleData:true}") boolean generateSampleData) + throws SQLException { + DbSchemaCreator dbSchemaCreator = + new DbSchemaCreator(dataSource, taskanaEngineConfiguration.getSchemaName()); + dbSchemaCreator.run(); + SampleDataGenerator sampleDataGenerator = + new SampleDataGenerator(dataSource, taskanaEngineConfiguration.getSchemaName()); if (generateSampleData) { sampleDataGenerator.generateSampleData(); } 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 @Bean(initMethod = "start", destroyMethod = "stop") public Server inMemoryH2DatabaseaServer() throws SQLException { diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/RestConfiguration.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/RestConfiguration.java index 382c24d08..3130d5a71 100644 --- a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/RestConfiguration.java +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/RestConfiguration.java @@ -53,6 +53,7 @@ public class RestConfiguration { } @Bean + @ConditionalOnMissingBean(TaskanaEngine.class) public TaskanaEngine getTaskanaEngine(TaskanaEngineConfiguration taskanaEngineConfiguration) throws SQLException { return taskanaEngineConfiguration.buildTaskanaEngine(); diff --git a/taskana-routing-parent/pom.xml b/taskana-routing-parent/pom.xml new file mode 100644 index 000000000..95f0f9022 --- /dev/null +++ b/taskana-routing-parent/pom.xml @@ -0,0 +1,27 @@ + + + + 4.0.0 + + taskana-routing-parent + + pom + + ${project.groupId}:${project.artifactId} + This pom is parent to all taskana routing modules + + + pro.taskana + taskana-parent + 4.5.2-SNAPSHOT + ../pom.xml + + + + taskana-spi-routing-dmn-router + + + + diff --git a/taskana-routing-parent/taskana-spi-routing-dmn-router/pom.xml b/taskana-routing-parent/taskana-spi-routing-dmn-router/pom.xml new file mode 100644 index 000000000..d48643f34 --- /dev/null +++ b/taskana-routing-parent/taskana-spi-routing-dmn-router/pom.xml @@ -0,0 +1,63 @@ + + + + taskana-routing-parent + pro.taskana + 4.5.2-SNAPSHOT + + 4.0.0 + + taskana-spi-routing-dmn-router + + + + pro.taskana + taskana-core + ${project.version} + + + org.camunda.bpm.model + camunda-dmn-model + ${version.camunda.dmn} + + + org.camunda.bpm.dmn + camunda-engine-dmn + ${version.camunda.dmn} + + + org.slf4j + slf4j-api + + + + + + pro.taskana + taskana-common-data + ${project.version} + test + + + pro.taskana + taskana-common-test + ${project.version} + test + + + org.junit.jupiter + junit-jupiter + test + + + com.h2database + h2 + test + + + + + + diff --git a/taskana-routing-parent/taskana-spi-routing-dmn-router/src/main/java/pro/taskana/routing/dmn/DmnTaskRouter.java b/taskana-routing-parent/taskana-spi-routing-dmn-router/src/main/java/pro/taskana/routing/dmn/DmnTaskRouter.java new file mode 100644 index 000000000..3438bb1ef --- /dev/null +++ b/taskana-routing-parent/taskana-spi-routing-dmn-router/src/main/java/pro/taskana/routing/dmn/DmnTaskRouter.java @@ -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> getAllWorkbasketAndDomainOutputs(DmnModelInstance dmnModel) { + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Entering getAllWorkbasketAndDomainOutputs()"); + } + + Set> allWorkbasketAndDomainOutputs = new HashSet<>(); + + for (Rule rule : dmnModel.getModelElementsByType(Rule.class)) { + + List 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> allWorkbasketAndDomainOutputs = + getAllWorkbasketAndDomainOutputs(dmnModel); + + validate(allWorkbasketAndDomainOutputs); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Exiting validateOutputs()"); + } + } + + private void validate(Set> allWorkbasketAndDomainOutputs) { + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "Entering validate(allWorkbasketAndDomainOutputs = {}", allWorkbasketAndDomainOutputs); + } + + WorkbasketService workbasketService = taskanaEngine.getWorkbasketService(); + + for (Pair 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()"); + } + } +} diff --git a/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/java/acceptance/AbstractAccTest.java b/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/java/acceptance/AbstractAccTest.java new file mode 100644 index 000000000..ca6d39103 --- /dev/null +++ b/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/java/acceptance/AbstractAccTest.java @@ -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; + } +} diff --git a/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/java/pro/taskana/routing/dmn/DmnTaskRouterTest.java b/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/java/pro/taskana/routing/dmn/DmnTaskRouterTest.java new file mode 100644 index 000000000..ae825db36 --- /dev/null +++ b/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/java/pro/taskana/routing/dmn/DmnTaskRouterTest.java @@ -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"); + } +} diff --git a/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/resources/META-INF/services/pro.taskana.spi.routing.api.TaskRoutingProvider b/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/resources/META-INF/services/pro.taskana.spi.routing.api.TaskRoutingProvider new file mode 100644 index 000000000..55b1fc4c7 --- /dev/null +++ b/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/resources/META-INF/services/pro.taskana.spi.routing.api.TaskRoutingProvider @@ -0,0 +1 @@ +pro.taskana.routing.dmn.DmnTaskRouter diff --git a/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/resources/dmn-table.dmn b/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/resources/dmn-table.dmn new file mode 100644 index 000000000..f0ece32d7 --- /dev/null +++ b/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/resources/dmn-table.dmn @@ -0,0 +1,64 @@ + + + + + + + + + + task.primaryObjRef.value + + + + + task.primaryObjRef.type + + + + + + + "00000001" + + + "MyType1" + + + "GPK_KSC" + + + "DOMAIN_A" + + + + + "00000001" + + + + + + "GPK_KSC_1" + + + "DOMAIN_A" + + + + + + + + "MyType1" + + + "GPK_KSC_2" + + + "DOMAIN_A" + + + + + diff --git a/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/resources/taskana.properties b/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/resources/taskana.properties new file mode 100644 index 000000000..a865d0d56 --- /dev/null +++ b/taskana-routing-parent/taskana-spi-routing-dmn-router/src/test/resources/taskana.properties @@ -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