TSK-1759: Convert Excel with routing rules to DMN and upload it to configurable path

This commit is contained in:
Joerg Heffner 2021-07-30 10:07:38 +02:00 committed by gitgoodjhe
parent 97b5c73158
commit 84558865e5
33 changed files with 1269 additions and 1 deletions

View File

@ -241,6 +241,7 @@ jobs:
- taskana-spring
- taskana-spring-example
- taskana-spi-routing-dmn-router
- taskana-routing-rest
- taskana-rest-spring
- taskana-rest-spring-example-common
- taskana-loghistory-provider

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All in taskana-routing-rest" type="JUnit" factoryName="JUnit" folderName="routing" nameIsGenerated="true">
<module name="taskana-routing-rest" />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="package" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@ -66,6 +66,11 @@
<artifactId>taskana-rest-spring</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>pro.taskana</groupId>
<artifactId>taskana-routing-rest</artifactId>
<version>${project.version}</version>
</dependency>
<!-- all SPI dependencies -->
<dependency>

View File

@ -5,6 +5,7 @@ BASE_URL=https://taskana.mybluemix.net/taskana
test 200 -eq $(curl -sw %{http_code} -o /dev/null "$BASE_URL/docs/rest/rest-api.html")
test 200 -eq $(curl -sw %{http_code} -o /dev/null "$BASE_URL/docs/rest/simplehistory-rest-api.html")
test 200 -eq $(curl -sw %{http_code} -o /dev/null "$BASE_URL/docs/rest/routing-rest-api.html")
for module in taskana-core taskana-spring; do
test 200 -eq $(curl -sw %{http_code} -o /dev/null "$BASE_URL/docs/java/$module/pro/taskana/package-summary.html")
done

View File

@ -20,5 +20,6 @@ verifyDocs "$REL/../lib/taskana-cdi/target/apidocs" "/static/docs/java/taskana-c
verifyDocs "$REL/../lib/taskana-spring/target/apidocs" "/static/docs/java/taskana-spring"
test -n "$(jar -tf $JAR_FILE_LOCATION | grep /static/docs/rest/rest-api.html)"
test -n "$(jar -tf $JAR_FILE_LOCATION | grep /static/docs/rest/simplehistory-rest-api.html)"
test -n "$(jar -tf $JAR_FILE_LOCATION | grep /static/docs/rest/routing-rest-api.html)"
set +x
echo "the jar file '$JAR_FILE_LOCATION' contains documentation"

View File

@ -94,6 +94,11 @@
<version.aspectj-maven-plugin>1.14.0</version.aspectj-maven-plugin>
<version.aspectj>1.9.7</version.aspectj>
<!-- Excel to DMN converter dependencies -->
<version.dmn-xlsx-converter>0.3.0</version.dmn-xlsx-converter>
<version.jaxb>2.3.0</version.jaxb>
<version.javax.activation>1.1.1</version.javax.activation>
<!-- database driver versions -->
<version.db2>11.1.1.1</version.db2>

View File

@ -18,6 +18,7 @@
<modules>
<module>taskana-spi-routing-dmn-router</module>
<module>taskana-routing-rest</module>
</modules>
</project>

View File

@ -0,0 +1,224 @@
<?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-rest</artifactId>
<name>${project.groupId}:${project.artifactId}</name>
<description>Task routing REST API implementation.</description>
<parent>
<groupId>pro.taskana</groupId>
<artifactId>taskana-routing-parent</artifactId>
<version>4.10.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>pro.taskana</groupId>
<artifactId>taskana-common-logging</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>pro.taskana</groupId>
<artifactId>taskana-rest-spring</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.camunda.bpm.extension.dmn</groupId>
<artifactId>dmn-xlsx-converter</artifactId>
<version>${version.dmn-xlsx-converter}</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${version.jaxb}</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>${version.jaxb}</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>${version.jaxb}</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>${version.javax.activation}</version>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>pro.taskana</groupId>
<artifactId>taskana-common-test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>pro.taskana</groupId>
<artifactId>taskana-common-data</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
<version>${version.spring.core}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>capital.scalable</groupId>
<artifactId>spring-auto-restdocs-core</artifactId>
<version>${version.auto-restdocs}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${version.maven.javadoc}</version>
<extensions>true</extensions>
<configuration>
<tags>
<tag>
<name>title</name>
<placement>m</placement>
</tag>
</tags>
</configuration>
<executions>
<execution>
<id>generate-javadoc-json</id>
<phase>validate</phase>
<goals>
<goal>javadoc-no-fork</goal>
</goals>
<configuration>
<doclet>capital.scalable.restdocs.jsondoclet.ExtractDocumentationAsJsonDoclet</doclet>
<docletArtifact>
<groupId>capital.scalable</groupId>
<!--
currently the jdk9+ version of this doclet has a very bad bug.
see: https://github.com/ScaCap/spring-auto-restdocs/issues/412
-->
<artifactId>spring-auto-restdocs-json-doclet</artifactId>
<version>${version.auto-restdocs}</version>
</docletArtifact>
<destDir>generated-javadoc-json</destDir>
<reportOutputDirectory>${project.build.directory}</reportOutputDirectory>
<useStandardDocletOptions>false</useStandardDocletOptions>
<show>package</show>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>${version.maven.asciidoctor}</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
</execution>
</executions>
<configuration>
<backend>html5</backend>
<doctype>book</doctype>
<attributes>
<snippets>${project.build.directory}/generated-snippets</snippets>
<doctype>book</doctype>
<icons>font</icons>
<source-highlighter>highlightjs</source-highlighter>
<toc>left</toc>
<docinfo>shared</docinfo>
<toclevels>4</toclevels>
<sectlinks/>
</attributes>
<logHandler>
<outputToConsole>false</outputToConsole>
<failIf>
<severity>ERROR</severity>
</failIf>
</logHandler>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,90 @@
<!-- Sourcecode at https://stackoverflow.com/questions/34481638/how-to-use-tocify-with-asciidoctor-for-a-dynamic-toc -->
<!-- Generate a nice TOC -->
<script src="jquery-1.12.4.min.js"></script>
<script src="jquery-ui.min.js"></script>
<script src="jquery.tocify.min.js"></script>
<!-- We do not need the tocify CSS because the asciidoc CSS already provides most of what we need -->
<style>
.tocify-header {
font-style: italic;
}
.tocify-subheader {
font-style: normal;
font-size: 100%;
}
.tocify ul {
margin: 0;
}
.tocify-focus {
color: #7a2518;
background-color: rgba(0, 0, 0, 0.1);
}
.tocify-focus > a {
color: #7a2518;
}
@media only screen and (min-width: 1750px) {
#toc.toc2 {
width: 25em;
}
#header, #content, #footer, #footnotes {
max-width: 80em;
}
}
.sect1:not(#_overview) .sect2 + .sect2 {
margin-top: 5em;
}
</style>
<script type="text/javascript">
$(function () {
// Add a new container for the tocify toc into the existing toc so we can re-use its
// styling
$("#toc").append("<div id='generated-toc'></div>");
$("#generated-toc").tocify({
extendPage: true,
context: "#content",
highlightOnScroll: true,
hideEffect: "slideUp",
// Use the IDs that asciidoc already provides so that TOC links and intra-document
// links are the same. Anything else might confuse users when they create bookmarks.
hashGenerator: function (text, element) {
return $(element).attr("id");
},
// Smooth scrolling doesn't work properly if we use the asciidoc IDs
smoothScroll: false,
// Set to 'none' to use the tocify classes
theme: "none",
// Handle book (may contain h1) and article (only h2 deeper)
selectors: $("#content").has("h1").size() > 0 ? "h1,h2,h3,h4,h5" : "h2,h3,h4,h5",
ignoreSelector: ".discrete"
});
// Switch between static asciidoc toc and dynamic tocify toc based on browser size
// This is set to match the media selectors in the asciidoc CSS
// Without this, we keep the dynamic toc even if it is moved from the side to preamble
// position which will cause odd scrolling behavior
const handleTocOnResize = function () {
if ($(document).width() < 768) {
$("#generated-toc").hide();
$(".sectlevel0").show();
$(".sectlevel1").show();
} else {
$("#generated-toc").show();
$(".sectlevel0").hide();
$(".sectlevel1").hide();
}
}
$(window).resize(handleTocOnResize);
handleTocOnResize();
});
</script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,26 @@
= TASKANA RESTful API Documentation
== Overview
This is the REST documentation for http://taskana.pro)[TASKANA]'s routing REST endpoints.
*For all Query Parameters:* whenever a parameter is an array type, several values can be passed by declaring that parameter multiple times.
=== Hypermedia Support
NOTE: HATEOAS support is still in development.
Please have a look at example responses for each resource to determine the available links.
TASKANA uses the https://restfulapi.net/hateoas/)[HATEOAS] (Hypermedia as the Engine of Application State) REST constraint.
Most of our resources contain a `_links` section which contains navigation links.
Besides, helping to navigate through our REST API, the navigation links also encapsulate the API.
Using HATEOAS allows us to change some endpoints without modifying your frontend.
=== Errors
In order to support multilingual websites, TASKANA uses error codes to define which error occurred.
Additionally, an optional set of message variables, containing some technical information, is added, so that the website can describe the error with all details.
== DMN routing Upload
include::{snippets}/DmnUploadControllerRestDocTest/convertAndUploadDocTest/auto-section.adoc[]

View File

@ -0,0 +1,64 @@
package pro.taskana.routing.dmn.rest;
import java.io.IOException;
import org.camunda.bpm.model.dmn.DmnModelInstance;
import org.camunda.bpm.model.dmn.instance.Rule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import pro.taskana.common.api.exceptions.NotAuthorizedException;
import pro.taskana.routing.dmn.service.DmnConverterService;
import pro.taskana.routing.dmn.spi.internal.DmnValidatorManager;
/** Controller for all DMN upload related endpoints. */
@RestController
public class DmnUploadController {
private final DmnConverterService dmnConverterService;
@Autowired
public DmnUploadController(DmnConverterService dmnConverterService) {
this.dmnConverterService = dmnConverterService;
}
/**
* This endpoint converts an excel file to a DMN table and saves it on the filesystem.
*
* @param excelRoutingFile the excel file containing the routing rules
* @return the result of the upload
* @throws NotAuthorizedException if the current user is not authorized to upload/convert an excel
* file
* @throws IOException if there is an I/O problem with the provided excel file
*/
@PutMapping(RoutingRestEndpoints.URL_ROUTING_RULES_DEFAULT)
public ResponseEntity<RoutingUploadResultRepresentationModel> convertAndUpload(
@RequestParam("excelRoutingFile") MultipartFile excelRoutingFile)
throws IOException, NotAuthorizedException {
DmnModelInstance dmnModelInstance = dmnConverterService.convertExcelToDmn(excelRoutingFile);
int importedRows = dmnModelInstance.getModelElementsByType(Rule.class).size();
RoutingUploadResultRepresentationModel model = new RoutingUploadResultRepresentationModel();
model.setAmountOfImportedRows(importedRows);
model.setResult(
"Successfully imported " + importedRows + " routing rules from the provided excel sheet");
return ResponseEntity.ok(model);
}
/**
* This endpoint checks if the taskana-routing-rest is in use.
*
* @return true, when the taskana-routing-rest is enabled, otherwise false
*/
@GetMapping(path = RoutingRestEndpoints.ROUTING_REST_ENABLED)
public ResponseEntity<Boolean> getIsRoutingRestEnabled() {
return ResponseEntity.ok(DmnValidatorManager.isDmnUploadProviderEnabled());
}
}

View File

@ -0,0 +1,12 @@
package pro.taskana.routing.dmn.rest;
public class RoutingRestEndpoints {
public static final String API_V1 = "/api/v1/";
public static final String URL_ROUTING_RULES = API_V1 + "routing-rules";
public static final String URL_ROUTING_RULES_DEFAULT = URL_ROUTING_RULES + "/default";
public static final String ROUTING_REST_ENABLED = URL_ROUTING_RULES + "routing-rest-enabled";
private RoutingRestEndpoints() {}
}

View File

@ -0,0 +1,30 @@
package pro.taskana.routing.dmn.rest;
import org.springframework.hateoas.RepresentationModel;
/** Model class for a routing upload result. */
public class RoutingUploadResultRepresentationModel
extends RepresentationModel<RoutingUploadResultRepresentationModel> {
/** The total amount of imported rows from the provided excel sheet. */
protected int amountOfImportedRows;
/** A human readable String that contains the amount of imported rows. */
protected String result;
public int getAmountOfImportedRows() {
return amountOfImportedRows;
}
public void setAmountOfImportedRows(int amountOfImportedRows) {
this.amountOfImportedRows = amountOfImportedRows;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
}

View File

@ -0,0 +1,126 @@
package pro.taskana.routing.dmn.service;
import java.io.BufferedInputStream;
import java.io.File;
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 java.util.stream.Collectors;
import org.camunda.bpm.dmn.xlsx.AdvancedSpreadsheetAdapter;
import org.camunda.bpm.dmn.xlsx.XlsxConverter;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import pro.taskana.common.api.KeyDomain;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.TaskanaRole;
import pro.taskana.common.api.exceptions.NotAuthorizedException;
import pro.taskana.common.api.exceptions.SystemException;
import pro.taskana.routing.dmn.service.util.InputEntriesSanitizer;
import pro.taskana.routing.dmn.spi.internal.DmnValidatorManager;
import pro.taskana.workbasket.api.WorkbasketService;
/** This class converts an Excel file with routing roules to a DMN table. */
@Service
public class DmnConverterService {
private static final Logger LOGGER = LoggerFactory.getLogger(DmnConverterService.class);
private final TaskanaEngine taskanaEngine;
@Value("${taskana.routing.dmn.upload.path}")
private String dmnUploadPath;
@Autowired
public DmnConverterService(TaskanaEngine taskanaEngine) {
this.taskanaEngine = taskanaEngine;
DmnValidatorManager.getInstance(taskanaEngine);
}
public String getDmnUploadPath() {
return dmnUploadPath;
}
public void setDmnUploadPath(String dmnUploadPath) {
this.dmnUploadPath = dmnUploadPath;
}
public DmnModelInstance convertExcelToDmn(MultipartFile excelRoutingFile)
throws IOException, NotAuthorizedException {
taskanaEngine.checkRoleMembership(TaskanaRole.ADMIN, TaskanaRole.BUSINESS_ADMIN);
try (InputStream inputStream = new BufferedInputStream(excelRoutingFile.getInputStream())) {
XlsxConverter converter = new XlsxConverter();
converter.setIoDetectionStrategy(new AdvancedSpreadsheetAdapter());
DmnModelInstance dmnModelInstance = converter.convert(inputStream);
validateOutputs(dmnModelInstance);
InputEntriesSanitizer.sanitizeRegexInsideInputEntries(dmnModelInstance);
if (DmnValidatorManager.isDmnUploadProviderEnabled()) {
DmnValidatorManager.getInstance(taskanaEngine).validate(dmnModelInstance);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("Persisting generated DMN table to %s", dmnUploadPath));
}
File uploadDestinationFile = new File(dmnUploadPath);
Dmn.writeModelToFile(uploadDestinationFile, dmnModelInstance);
return dmnModelInstance;
}
}
private Set<KeyDomain> getOutputKeyDomains(DmnModelInstance dmnModel) {
Set<KeyDomain> outputKeyDomains = new HashSet<>();
for (Rule rule : dmnModel.getModelElementsByType(Rule.class)) {
List<OutputEntry> outputEntries = new ArrayList<>(rule.getOutputEntries());
String workbasketKey = outputEntries.get(0).getTextContent().replaceAll("(^\")|(\"$)", "");
String domain = outputEntries.get(1).getTextContent().replaceAll("(^\")|(\"$)", "");
outputKeyDomains.add(new KeyDomain(workbasketKey, domain));
}
return outputKeyDomains;
}
private void validateOutputs(DmnModelInstance dmnModel) {
Set<KeyDomain> outputKeyDomains = getOutputKeyDomains(dmnModel);
Set<KeyDomain> existingKeyDomains = getExistingKeyDomains();
outputKeyDomains.removeAll(existingKeyDomains);
if (!outputKeyDomains.isEmpty()) {
throw new SystemException(
String.format(
"Unknown workbasket Key/Domain pairs defined in DMN Table: %s", outputKeyDomains));
}
}
private Set<KeyDomain> getExistingKeyDomains() {
WorkbasketService workbasketService = taskanaEngine.getWorkbasketService();
return taskanaEngine.runAsAdmin(
() ->
workbasketService.createWorkbasketQuery().list().stream()
.map(
workbasketSummary ->
new KeyDomain(workbasketSummary.getKey(), workbasketSummary.getDomain()))
.collect(Collectors.toSet()));
}
}

View File

@ -0,0 +1,31 @@
package pro.taskana.routing.dmn.service.util;
import org.camunda.bpm.model.dmn.DmnModelInstance;
import org.camunda.bpm.model.dmn.instance.Rule;
import org.camunda.bpm.model.dmn.instance.Text;
/** Utility class to sanitize known regex calls inside a generated DmnModelInstance. */
public class InputEntriesSanitizer {
private InputEntriesSanitizer() {
throw new IllegalStateException("Utility class");
}
public static void sanitizeRegexInsideInputEntries(DmnModelInstance dmnModelInstance) {
dmnModelInstance
.getModelElementsByType(Rule.class)
.forEach(
rule ->
rule.getInputEntries()
.forEach(
inputEntry -> {
Text input = inputEntry.getText();
String inputTextContent = input.getTextContent();
if (inputTextContent.contains("matches")
|| inputTextContent.contains("contains")) {
input.setTextContent(inputTextContent.replaceAll("(^\")|(\"$)", ""));
}
inputEntry.setText(input);
}));
}
}

View File

@ -0,0 +1,23 @@
package pro.taskana.routing.dmn.spi.api;
import org.camunda.bpm.model.dmn.DmnModelInstance;
import pro.taskana.common.api.TaskanaEngine;
public interface DmnValidator {
/**
* Initialize DmnValidator.
*
* @param taskanaEngine {@link TaskanaEngine} The Taskana engine needed for initialization.
*/
void initialize(TaskanaEngine taskanaEngine);
/**
* Validates a DmnModelInstance.
*
* @param dmnModelInstance the DMN model to validate
*
*/
void validate(DmnModelInstance dmnModelInstance);
}

View File

@ -0,0 +1,58 @@
package pro.taskana.routing.dmn.spi.internal;
import java.util.Objects;
import java.util.ServiceLoader;
import org.camunda.bpm.model.dmn.DmnModelInstance;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.exceptions.SystemException;
import pro.taskana.routing.dmn.spi.api.DmnValidator;
/** Loads DmnValidator SPI implementation(s) and passes requests to validate DmnModelInstances. */
public class DmnValidatorManager {
private static final Logger LOGGER = LoggerFactory.getLogger(DmnValidatorManager.class);
private static DmnValidatorManager singleton;
private final ServiceLoader<DmnValidator> serviceLoader;
private boolean enabled = false;
private DmnValidatorManager(TaskanaEngine taskanaEngine) {
serviceLoader = ServiceLoader.load(DmnValidator.class);
for (DmnValidator dmnValidator : serviceLoader) {
dmnValidator.initialize(taskanaEngine);
LOGGER.info("Registered DmnValidator: {}", dmnValidator.getClass().getName());
enabled = true;
}
if (!enabled) {
LOGGER.info("No DmnValidator found. Running without DmnValidator.");
}
}
public static synchronized DmnValidatorManager getInstance(TaskanaEngine taskanaEngine) {
if (singleton == null) {
singleton = new DmnValidatorManager(taskanaEngine);
}
return singleton;
}
public static boolean isDmnUploadProviderEnabled() {
return Objects.nonNull(singleton) && singleton.enabled;
}
public void validate(DmnModelInstance dmnModelInstanceToValidate) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Sending DmnModelInstance to DmnValidators: {}", dmnModelInstanceToValidate);
}
serviceLoader.forEach(
dmnValidator -> {
try {
dmnValidator.validate(dmnModelInstanceToValidate);
} catch (Exception e) {
throw new SystemException("Caught exception while validating dmnModelInstance", e);
}
});
}
}

View File

@ -0,0 +1,75 @@
package pro.taskana.routing.dmn.rest;
import static pro.taskana.common.test.rest.RestHelper.TEMPLATE;
import java.io.File;
import javax.sql.DataSource;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import pro.taskana.common.test.rest.RestHelper;
import pro.taskana.common.test.rest.TaskanaSpringBootTest;
/** Test DmnUploadController. */
@TaskanaSpringBootTest
class DmnUploadControllerIntTest {
private static final String EXCEL_NAME = "testExcelRouting.xlsx";
private static final String HTTP_BODY_FILE_NAME = "excelRoutingFile";
private final RestHelper restHelper;
private final DataSource dataSource;
private final String schemaName;
@Autowired
DmnUploadControllerIntTest(
RestHelper restHelper,
DataSource dataSource,
@Value("${taskana.schemaName:TASKANA}") String schemaName) {
this.restHelper = restHelper;
this.dataSource = dataSource;
this.schemaName = schemaName;
}
@Test
void should_returnCorrectAmountOfImportedRoutingRules() throws Exception {
File excelRoutingFile = new ClassPathResource(EXCEL_NAME).getFile();
MultiValueMap<String, FileSystemResource> body = new LinkedMultiValueMap<>();
body.add(HTTP_BODY_FILE_NAME, new FileSystemResource(excelRoutingFile));
HttpHeaders headers = RestHelper.generateHeadersForUser("admin");
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<Object> auth = new HttpEntity<>(body, headers);
String url = restHelper.toUrl(RoutingRestEndpoints.URL_ROUTING_RULES_DEFAULT);
ResponseEntity<RoutingUploadResultRepresentationModel> responseEntity =
TEMPLATE.exchange(url, HttpMethod.PUT, auth, RoutingUploadResultRepresentationModel.class);
SoftAssertions softly = new SoftAssertions();
softly
.assertThat(responseEntity.getBody())
.extracting(RoutingUploadResultRepresentationModel::getAmountOfImportedRows)
.isEqualTo(3);
softly
.assertThat(responseEntity.getBody())
.extracting(RoutingUploadResultRepresentationModel::getResult)
.isEqualTo("Successfully imported 3 routing rules from the provided excel sheet");
softly.assertAll();
}
}

View File

@ -0,0 +1,39 @@
package pro.taskana.routing.dmn.rest;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import pro.taskana.common.test.BaseRestDocTest;
class DmnUploadControllerRestDocTest extends BaseRestDocTest {
private static final String EXCEL_NAME = "testExcelRouting.xlsx";
private static final String REST_REQUEST_PARAM_NAME = "excelRoutingFile";
@Test
void convertAndUploadDocTest() throws Exception {
File excelRoutingFile = new ClassPathResource(EXCEL_NAME).getFile();
InputStream targetStream = new FileInputStream(excelRoutingFile);
MockMultipartFile routingMultiPartFile =
new MockMultipartFile(REST_REQUEST_PARAM_NAME, targetStream);
mockMvc
.perform(
MockMvcRequestBuilders.multipart(RoutingRestEndpoints.URL_ROUTING_RULES_DEFAULT)
.file(routingMultiPartFile)
.with(
request -> {
request.setMethod("PUT");
return request;
}))
.andExpect(MockMvcResultMatchers.status().isOk());
}
}

View File

@ -0,0 +1,76 @@
package pro.taskana.routing.dmn.rest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import pro.taskana.common.rest.SpringSecurityToJaasFilter;
@EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
private final LdapAuthoritiesPopulator ldapAuthoritiesPopulator;
private final GrantedAuthoritiesMapper grantedAuthoritiesMapper;
private final String ldapServerUrl;
private final String ldapBaseDn;
private final String ldapGroupSearchBase;
private final String ldapUserDnPatterns;
@Autowired
public WebSecurityConfigurer(
@Value("${taskana.ldap.serverUrl:ldap://localhost:10389}") String ldapServerUrl,
@Value("${taskana.ldap.baseDn:OU=Test,O=TASKANA}") String ldapBaseDn,
@Value("${taskana.ldap.groupSearchBase:cn=groups}") String ldapGroupSearchBase,
@Value("${taskana.ldap.userDnPatterns:uid={0},cn=users}") String ldapUserDnPatterns,
LdapAuthoritiesPopulator ldapAuthoritiesPopulator,
GrantedAuthoritiesMapper grantedAuthoritiesMapper) {
this.ldapServerUrl = ldapServerUrl;
this.ldapBaseDn = ldapBaseDn;
this.ldapGroupSearchBase = ldapGroupSearchBase;
this.ldapUserDnPatterns = ldapUserDnPatterns;
this.ldapAuthoritiesPopulator = ldapAuthoritiesPopulator;
this.grantedAuthoritiesMapper = grantedAuthoritiesMapper;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userDnPatterns(ldapUserDnPatterns)
.groupSearchBase(ldapGroupSearchBase)
.ldapAuthoritiesPopulator(ldapAuthoritiesPopulator)
.authoritiesMapper(grantedAuthoritiesMapper)
.contextSource()
.url(ldapServerUrl + "/" + ldapBaseDn)
.and()
.passwordCompare()
.passwordAttribute("userPassword");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.and()
.csrf()
.disable()
.httpBasic()
.and()
.addFilter(jaasApiIntegrationFilter())
.addFilterAfter(new SpringSecurityToJaasFilter(), JaasApiIntegrationFilter.class)
.authorizeRequests()
.anyRequest()
.fullyAuthenticated();
}
private JaasApiIntegrationFilter jaasApiIntegrationFilter() {
JaasApiIntegrationFilter filter = new JaasApiIntegrationFilter();
filter.setCreateEmptySubject(true);
return filter;
}
}

View File

@ -0,0 +1,114 @@
package pro.taskana.routing.dmn.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import javax.sql.DataSource;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.camunda.bpm.model.dmn.DmnModelInstance;
import org.camunda.bpm.model.dmn.instance.Rule;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import pro.taskana.TaskanaEngineConfiguration;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.TaskanaEngine.ConnectionManagementMode;
import pro.taskana.common.api.exceptions.NotAuthorizedException;
import pro.taskana.common.api.exceptions.TaskanaRuntimeException;
import pro.taskana.common.test.config.DataSourceGenerator;
import pro.taskana.common.test.security.JaasExtension;
import pro.taskana.common.test.security.WithAccessId;
import pro.taskana.sampledata.SampleDataGenerator;
@ExtendWith(JaasExtension.class)
class DmnConverterServiceAccTest {
private static final String EXCEL_NAME = "testExcelRouting.xlsx";
private static final String EXCEL_NAME_INVALID_OUTPUTS =
"testExcelRoutingWithInvalidOutputs.xlsx";
private static TaskanaEngine taskanaEngine;
@BeforeAll
protected static void setupTest() throws Exception {
resetDb(false);
}
protected static void resetDb(boolean dropTables) throws Exception {
DataSource dataSource = DataSourceGenerator.getDataSource();
String schemaName = DataSourceGenerator.getSchemaName();
TaskanaEngineConfiguration taskanaEngineConfiguration =
new TaskanaEngineConfiguration(dataSource, false, schemaName);
taskanaEngineConfiguration.setGermanPublicHolidaysEnabled(true);
SampleDataGenerator sampleDataGenerator =
new SampleDataGenerator(dataSource, taskanaEngineConfiguration.getSchemaName());
if (dropTables) {
sampleDataGenerator.dropDb();
}
taskanaEngine =
taskanaEngineConfiguration.buildTaskanaEngine(ConnectionManagementMode.AUTOCOMMIT);
sampleDataGenerator.clearDb();
sampleDataGenerator.generateTestData();
}
@Test
@WithAccessId(user = "businessadmin")
void should_ConvertExcelToDmn() throws Exception {
File excelRoutingFile = new ClassPathResource(EXCEL_NAME).getFile();
InputStream targetStream = new FileInputStream(excelRoutingFile);
MultipartFile routingMultiPartFile = new MockMultipartFile(EXCEL_NAME, targetStream);
DmnConverterService dmnConverterService = new DmnConverterService(taskanaEngine);
dmnConverterService.setDmnUploadPath("target\\routing.dmn");
DmnModelInstance dmnModelInstance = dmnConverterService.convertExcelToDmn(routingMultiPartFile);
assertThat(dmnModelInstance.getModelElementsByType(Rule.class)).hasSize(3);
}
@Test
@WithAccessId(user = "businessadmin")
void should_ThrowException_When_ProvidingInvalidOutputKeyDomains() throws Exception {
File excelRoutingFile = new ClassPathResource(EXCEL_NAME_INVALID_OUTPUTS).getFile();
InputStream targetStream = new FileInputStream(excelRoutingFile);
MultipartFile routingMultiPartFile =
new MockMultipartFile(EXCEL_NAME_INVALID_OUTPUTS, targetStream);
DmnConverterService dmnConverterService = new DmnConverterService(taskanaEngine);
ThrowingCallable call = () -> dmnConverterService.convertExcelToDmn(routingMultiPartFile);
assertThatThrownBy(call)
.extracting(TaskanaRuntimeException.class::cast)
.extracting(TaskanaRuntimeException::getMessage)
.isEqualTo(
"Unknown workbasket Key/Domain pairs defined in DMN Table: "
+ "[KeyDomain [key=GPK_KSC1, domain=DOMAIN_A], "
+ "KeyDomain [key=GPK_KSC, domain=DOMAIN_XZ]]");
}
@Test
@WithAccessId(user = "user-1-1")
void should_ThrowException_When_NotAuthorized() throws Exception {
File excelRoutingFile = new ClassPathResource(EXCEL_NAME).getFile();
InputStream targetStream = new FileInputStream(excelRoutingFile);
MultipartFile routingMultiPartFile = new MockMultipartFile(EXCEL_NAME, targetStream);
DmnConverterService dmnConverterService = new DmnConverterService(taskanaEngine);
ThrowingCallable call = () -> dmnConverterService.convertExcelToDmn(routingMultiPartFile);
assertThatThrownBy(call).isInstanceOf(NotAuthorizedException.class);
}
}

View File

@ -0,0 +1,67 @@
package pro.taskana.routing.dmn.service.util;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.camunda.bpm.model.dmn.Dmn;
import org.camunda.bpm.model.dmn.DmnModelInstance;
import org.camunda.bpm.model.dmn.instance.InputEntry;
import org.camunda.bpm.model.dmn.instance.Rule;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
/** Test InputEntriesSanitizer. */
class InputEntriesSanitizerAccTest {
private static final String TEST_DMN = "testDmnRouting.dmn";
@Test
void should_SanitizeInputEntries_When_ContainingRegex() throws Exception {
File testDmnModel = new ClassPathResource(TEST_DMN).getFile();
DmnModelInstance dmnModelInstance = Dmn.readModelFromFile(testDmnModel);
List<Rule> allRules =
dmnModelInstance.getModelElementsByType(Rule.class).stream().collect(Collectors.toList());
List<InputEntry> inputEntriesOfFirstRuleToSanitize =
new ArrayList(allRules.get(1).getInputEntries());
List<InputEntry> inputEntriesOfSecondRuleToSanitize =
new ArrayList(allRules.get(2).getInputEntries());
String inputEntryContainingMatchesFunction =
inputEntriesOfFirstRuleToSanitize.get(1).getTextContent();
assertThat(inputEntryContainingMatchesFunction)
.isEqualTo("\"matches(cellInput,\"12924|12925|\")\"");
String inputEntryContainingContainsFunction =
inputEntriesOfSecondRuleToSanitize.get(1).getTextContent();
assertThat(inputEntryContainingContainsFunction).isEqualTo("\"contains(\"someString\")\"");
InputEntriesSanitizer.sanitizeRegexInsideInputEntries(dmnModelInstance);
List<Rule> allRulesAfterSanitzing =
dmnModelInstance.getModelElementsByType(Rule.class).stream().collect(Collectors.toList());
List<InputEntry> inputEntriesOfFirstRuleAfterSanitizing =
new ArrayList(allRulesAfterSanitzing.get(1).getInputEntries());
inputEntryContainingMatchesFunction =
inputEntriesOfFirstRuleAfterSanitizing.get(1).getTextContent();
assertThat(inputEntryContainingMatchesFunction)
.isEqualTo("matches(cellInput,\"12924|12925|\")");
List<InputEntry> inputEntriesOfSecondRuleAfterSanitizing =
new ArrayList(allRulesAfterSanitzing.get(2).getInputEntries());
inputEntryContainingContainsFunction =
inputEntriesOfSecondRuleAfterSanitizing.get(1).getTextContent();
assertThat(inputEntryContainingContainsFunction).isEqualTo("contains(\"someString\")");
}
}

View File

@ -0,0 +1,19 @@
package pro.taskana.routing.dmn.spi;
import org.camunda.bpm.model.dmn.DmnModelInstance;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.routing.dmn.spi.api.DmnValidator;
public class TestDmnValidatorImpl implements DmnValidator {
@Override
public void initialize(TaskanaEngine taskanaEngine) {
}
@Override
public void validate(DmnModelInstance dmnModelInstance) {
//custom validation logic
}
}

View File

@ -0,0 +1 @@
pro.taskana.routing.dmn.spi.TestDmnValidatorImpl

View File

@ -0,0 +1,62 @@
logging.level.pro.taskana=INFO
logging.level.org.springframework.security=INFO
######## Taskana DB #######
spring.datasource.url=jdbc:h2:mem:taskana;IGNORECASE=TRUE;LOCK_MODE=0;
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=sa
taskana.schemaName=TASKANA
####### property that control rest api security deploy use true for no security.
devMode=false
####property that specifies the upload path for routing rules
taskana.routing.dmn.upload.path=target\\routing.dmn
####### Properties for AccessIdController to connect to LDAP
taskana.ldap.serverUrl=ldap://localhost:10389
taskana.ldap.bindDn=uid=admin
taskana.ldap.bindPassword=secret
taskana.ldap.baseDn=ou=Test,O=TASKANA
taskana.ldap.userSearchBase=cn=users
taskana.ldap.userSearchFilterName=objectclass
taskana.ldap.userSearchFilterValue=person
taskana.ldap.userFirstnameAttribute=givenName
taskana.ldap.userLastnameAttribute=sn
taskana.ldap.userFullnameAttribute=cn
taskana.ldap.userPhoneAttribute=phoneNumber
taskana.ldap.userMobilePhoneAttribute=mobileNumber
taskana.ldap.userEmailAttribute=email
taskana.ldap.userOrglevel1Attribute=orgLevel1
taskana.ldap.userOrglevel2Attribute=orgLevel2
taskana.ldap.userOrglevel3Attribute=someDepartement
taskana.ldap.userOrglevel4Attribute=orgLevel4
taskana.ldap.userIdAttribute=uid
taskana.ldap.userMemberOfGroupAttribute=memberOf
taskana.ldap.groupSearchBase=
taskana.ldap.groupSearchFilterName=objectclass
taskana.ldap.groupSearchFilterValue=groupOfUniqueNames
taskana.ldap.groupNameAttribute=cn
taskana.ldap.minSearchForLength=3
taskana.ldap.maxNumberOfReturnedAccessIds=50
taskana.ldap.groupsOfUser=uniquemember
# Embedded Spring LDAP server
spring.ldap.embedded.base-dn= OU=Test,O=TASKANA
spring.ldap.embedded.credential.username= uid=admin
spring.ldap.embedded.credential.password= secret
spring.ldap.embedded.ldif=classpath:taskana-test.ldif
spring.ldap.embedded.port= 10389
spring.ldap.embedded.validation.enabled=false
# Do not serialize null values for the documentation.
spring.jackson.default-property-inclusion=non_null
####### JobScheduler cron expression that specifies when the JobSchedler runs
taskana.jobscheduler.async.cron=0 0 * * * *
####### cache static resources properties
spring.web.resources.cache.cachecontrol.cache-private=true
spring.main.allow-bean-definition-overriding=true
####### tomcat is not detecting the x-forward headers from bluemix as a trustworthy proxy
server.tomcat.remoteip.internal-proxies=.*
server.forward-headers-strategy=native

View File

@ -0,0 +1,19 @@
taskana.roles.user=cn=ksc-users,cn=groups,OU=Test,O=TASKANA | 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,DOMAIN_TEST
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.german.holidays.enabled=true
taskana.history.deletion.on.task.deletion.enabled=true

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" id="definitions" name="definitions" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.3.5">
<decision id="workbasketRouting" name="workbasketRouting">
<decisionTable id="decisionTable" hitPolicy="FIRST">
<input id="input1" label="BKN">
<inputExpression id="InputExpression1816276523" typeRef="string">
<text>task.primaryObjRef.value</text>
</inputExpression>
</input>
<input id="input2" label="ClassificationKey">
<inputExpression id="InputExpression475650354" typeRef="string" expressionLanguage="javascript">
<text>task.classificationSummary.key + task.note</text>
</inputExpression>
</input>
<output id="output1" label="workbasketKey" name="workbasketKey" typeRef="string" />
<output id="output2" label="domain" name="domain" typeRef="string" />
<rule id="excelRow6">
<description>VIP-Team</description>
<inputEntry id="B6">
<text>"06260203"</text>
</inputEntry>
<inputEntry id="C6">
<text>-</text>
</inputEntry>
<outputEntry id="D6">
<text>"GPK_ARS"</text>
</outputEntry>
<outputEntry id="E6">
<text>"ULAK-D"</text>
</outputEntry>
</rule>
<rule id="excelRow7">
<description>Second-Level Team 1</description>
<inputEntry id="B7">
<text>-</text>
</inputEntry>
<inputEntry id="C7">
<text>"matches(cellInput,"12924|12925|")"</text>
</inputEntry>
<outputEntry id="D7">
<text>"GPK_ARS_4"</text>
</outputEntry>
<outputEntry id="E7">
<text>"ULAK-D"</text>
</outputEntry>
</rule>
<rule id="excelRow8">
<description>ULAK-D</description>
<inputEntry id="B8">
<text>"12345678"</text>
</inputEntry>
<inputEntry id="C8">
<text>"contains("someString")"</text>
</inputEntry>
<outputEntry id="D8">
<text>"GPK_ARS_3"</text>
</outputEntry>
<outputEntry id="E8">
<text>"ULAK-D"</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>