TSK-1759: Convert Excel with routing rules to DMN and upload it to configurable path
This commit is contained in:
parent
97b5c73158
commit
84558865e5
|
@ -241,6 +241,7 @@ jobs:
|
||||||
- taskana-spring
|
- taskana-spring
|
||||||
- taskana-spring-example
|
- taskana-spring-example
|
||||||
- taskana-spi-routing-dmn-router
|
- taskana-spi-routing-dmn-router
|
||||||
|
- taskana-routing-rest
|
||||||
- taskana-rest-spring
|
- taskana-rest-spring
|
||||||
- taskana-rest-spring-example-common
|
- taskana-rest-spring-example-common
|
||||||
- taskana-loghistory-provider
|
- taskana-loghistory-provider
|
||||||
|
|
|
@ -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>
|
|
@ -14,4 +14,4 @@
|
||||||
<envs />
|
<envs />
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
@ -66,6 +66,11 @@
|
||||||
<artifactId>taskana-rest-spring</artifactId>
|
<artifactId>taskana-rest-spring</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>pro.taskana</groupId>
|
||||||
|
<artifactId>taskana-routing-rest</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- all SPI dependencies -->
|
<!-- all SPI dependencies -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
@ -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/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/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
|
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")
|
test 200 -eq $(curl -sw %{http_code} -o /dev/null "$BASE_URL/docs/java/$module/pro/taskana/package-summary.html")
|
||||||
done
|
done
|
||||||
|
|
|
@ -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"
|
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/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/simplehistory-rest-api.html)"
|
||||||
|
test -n "$(jar -tf $JAR_FILE_LOCATION | grep /static/docs/rest/routing-rest-api.html)"
|
||||||
set +x
|
set +x
|
||||||
echo "the jar file '$JAR_FILE_LOCATION' contains documentation"
|
echo "the jar file '$JAR_FILE_LOCATION' contains documentation"
|
||||||
|
|
5
pom.xml
5
pom.xml
|
@ -94,6 +94,11 @@
|
||||||
<version.aspectj-maven-plugin>1.14.0</version.aspectj-maven-plugin>
|
<version.aspectj-maven-plugin>1.14.0</version.aspectj-maven-plugin>
|
||||||
<version.aspectj>1.9.7</version.aspectj>
|
<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 -->
|
<!-- database driver versions -->
|
||||||
<version.db2>11.1.1.1</version.db2>
|
<version.db2>11.1.1.1</version.db2>
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>taskana-spi-routing-dmn-router</module>
|
<module>taskana-spi-routing-dmn-router</module>
|
||||||
|
<module>taskana-routing-rest</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
@ -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[]
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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\")");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
pro.taskana.routing.dmn.spi.TestDmnValidatorImpl
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue