Merge branch 'taskana-md'

This commit is contained in:
Mustapha Zorgati 2021-01-07 15:16:40 +01:00
commit d4c70132f7
416 changed files with 13061 additions and 12025 deletions

8
NOTICE
View File

@ -1,8 +0,0 @@
Maven Artifact
Copyright 2001-2019 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).

View File

@ -53,7 +53,7 @@ function main() {
set -x
eval "$REL/prepare_db.sh '$1'"
### INSTALL ###
$REL/../mvnw -q install -B -T 2C -f $REL/.. -pl :taskana-rest-spring-example-common -am -P postgres -DskipTests -Dmaven.javadoc.skip -Dcheckstyle.skip -Dasciidoctor.skip
$REL/../mvnw -q install -B -T 2C -f $REL/.. -pl :taskana-rest-spring-example-common -am -DskipTests -Dmaven.javadoc.skip -Dcheckstyle.skip -Dasciidoctor.skip
### TEST ###
$REL/../mvnw -q verify -B -T 2C -f $REL/.. -pl :taskana-core -Dmaven.javadoc.skip -Dcheckstyle.skip
@ -82,7 +82,8 @@ function main() {
### TEST ###
(cd $REL/../web && npm run test -- --coverageReporters text-summary)
(cd $REL/../web && npm run e2e -- --config-file ../ci/cypress.json)
### TEMP REMOVE CYPRESS TESTS ###
### (cd $REL/../web && npm run e2e -- --config-file ../ci/cypress.json) ###
### CLEANUP ###
jobs -p | xargs -rn10 kill

View File

@ -4,7 +4,7 @@ set -x
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 -z "$(curl -s $BASE_URL/docs/rest/rest-api.html | grep 'Unresolved directive.*adoc')"
test 200 -eq $(curl -sw %{http_code} -o /dev/null "$BASE_URL/docs/rest/simplehistory-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

@ -1,61 +1,61 @@
-- KSC authorizations (ID , WB_ID , ACCESS_ID , ACCESS_NAME , READ , OPEN , APPEND, TRANSFER, DISTRIBUTE, C1, .., C12)
-- KSC authorizations (ID , WB_ID , ACCESS_ID , ACCESS_NAME , READ , OPEN , APPEND, TRANSFER, DISTRIBUTE, C1, .., C12)
-- PPKs
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000001', 'WBI:100000000000000000000000000000000004', 'teamlead-1' , 'Titus Toll' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000002', 'WBI:100000000000000000000000000000000005', 'teamlead-2' , 'Frauke Faul' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000006', 'user-1-1' , 'Max Mustermann' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000004', 'WBI:100000000000000000000000000000000007', 'user-1-2' , 'Elena Eifrig' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000005', 'WBI:100000000000000000000000000000000008', 'user-2-1' , 'Simone Müller' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000006', 'WBI:100000000000000000000000000000000009', 'user-2-2' , 'Tim Schläfrig' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016201', 'WBI:100000000000000000000000000000000016', 'user-2-1' , 'Simone Müller' , true , true , true , true , true , true , false , false , false , false , false , false , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016202', 'WBI:100000000000000000000000000000000016', 'user-2-2' , 'Tim Schläfrig' , true , true , true , true , true , false , true , false , false , false , false , false , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016203', 'WBI:100000000000000000000000000000000016', 'user-2-3' , 'Thomas Bach' , true , true , true , true , true , false , false , true , false , false , false , false , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016204', 'WBI:100000000000000000000000000000000016', 'user-2-4' , 'Rolf Wieland' , true , true , true , true , true , false , false , false , true , false , false , false , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016205', 'WBI:100000000000000000000000000000000016', 'user-2-5' , 'Heike Schmidt' , true , true , true , true , true , false , false , false , false , true , false , false , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016206', 'WBI:100000000000000000000000000000000016', 'user-2-6' , 'Kurt Maier' , true , true , true , true , true , false , false , false , false , false , true , false , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016207', 'WBI:100000000000000000000000000000000016', 'user-2-7' , 'Wiebke Meyer' , true , true , true , true , true , false , false , false , false , false , false , true , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016208', 'WBI:100000000000000000000000000000000016', 'user-2-8' , 'Jana Heeg' , true , true , true , true , true , false , false , false , false , false , false , false , true, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016209', 'WBI:100000000000000000000000000000000016', 'user-2-9' , 'Nathalie Fuchs' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016210', 'WBI:100000000000000000000000000000000016', 'user-2-10' , 'Johannes Renz' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:B00000000000000000000000000000000014', 'WBI:100000000000000000000000000000000014', 'user-b-1' , 'Bernd Bern' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:B00000000000000000000000000000000015', 'WBI:100000000000000000000000000000000015', 'user-b-2' , 'Brundhilde Bio' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000001', 'WBI:100000000000000000000000000000000004', 'teamlead-1' , 'Titus Toll' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000002', 'WBI:100000000000000000000000000000000005', 'teamlead-2' , 'Frauke Faul' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000006', 'user-1-1' , 'Max Mustermann' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000004', 'WBI:100000000000000000000000000000000007', 'user-1-2' , 'Elena Eifrig' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000005', 'WBI:100000000000000000000000000000000008', 'user-2-1' , 'Simone Müller' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000006', 'WBI:100000000000000000000000000000000009', 'user-2-2' , 'Tim Schläfrig' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016201', 'WBI:100000000000000000000000000000000016', 'user-2-1' , 'Simone Müller' , true , true , true , true , true , true , false , false , false , false , false , false , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016202', 'WBI:100000000000000000000000000000000016', 'user-2-2' , 'Tim Schläfrig' , true , true , true , true , true , false , true , false , false , false , false , false , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016203', 'WBI:100000000000000000000000000000000016', 'user-2-3' , 'Thomas Bach' , true , true , true , true , true , false , false , true , false , false , false , false , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016204', 'WBI:100000000000000000000000000000000016', 'user-2-4' , 'Rolf Wieland' , true , true , true , true , true , false , false , false , true , false , false , false , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016205', 'WBI:100000000000000000000000000000000016', 'user-2-5' , 'Heike Schmidt' , true , true , true , true , true , false , false , false , false , true , false , false , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016206', 'WBI:100000000000000000000000000000000016', 'user-2-6' , 'Kurt Maier' , true , true , true , true , true , false , false , false , false , false , true , false , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016207', 'WBI:100000000000000000000000000000000016', 'user-2-7' , 'Wiebke Meyer' , true , true , true , true , true , false , false , false , false , false , false , true , false, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016208', 'WBI:100000000000000000000000000000000016', 'user-2-8' , 'Jana Heeg' , true , true , true , true , true , false , false , false , false , false , false , false , true, true , true , true, true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016209', 'WBI:100000000000000000000000000000000016', 'user-2-9' , 'Nathalie Fuchs' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000016210', 'WBI:100000000000000000000000000000000016', 'user-2-10' , 'Johannes Renz' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:B00000000000000000000000000000000014', 'WBI:100000000000000000000000000000000014', 'user-b-1' , 'Bernd Bern' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:B00000000000000000000000000000000015', 'WBI:100000000000000000000000000000000015', 'user-b-2' , 'Brundhilde Bio' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
-- group internal access
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000007', 'WBI:100000000000000000000000000000000004', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 1' , true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000008', 'WBI:100000000000000000000000000000000005', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 2' , true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000009', 'WBI:100000000000000000000000000000000006', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 1' , true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000010', 'WBI:100000000000000000000000000000000007', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 1' , true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000011', 'WBI:100000000000000000000000000000000008', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 2' , true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000012', 'WBI:100000000000000000000000000000000009', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 2' , true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000007', 'WBI:100000000000000000000000000000000004', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 1', true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000008', 'WBI:100000000000000000000000000000000005', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 2', true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000009', 'WBI:100000000000000000000000000000000006', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 1', true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000010', 'WBI:100000000000000000000000000000000007', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 1', true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000011', 'WBI:100000000000000000000000000000000008', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 2', true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000012', 'WBI:100000000000000000000000000000000009', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 2', true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
-- teamlead substitution
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000013', 'WBI:100000000000000000000000000000000004', 'teamlead-2' , 'Frauke Faul' , true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000014', 'WBI:100000000000000000000000000000000005', 'teamlead-1' , 'Titus Toll' , true , true , true , false , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000013', 'WBI:100000000000000000000000000000000004', 'teamlead-2' , 'Frauke Faul' , true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000014', 'WBI:100000000000000000000000000000000005', 'teamlead-1' , 'Titus Toll' , true , true , true , false , false , false, false, false, false, false, false, false, false, false, false, false, false);
-- cross team tranfers
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000015', 'WBI:100000000000000000000000000000000006', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 2' , true , false, true , false , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000016', 'WBI:100000000000000000000000000000000007', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 2' , true , false, true , false , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000017', 'WBI:100000000000000000000000000000000008', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 1' , true , false, false , false , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000018', 'WBI:100000000000000000000000000000000009', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 1' , true , false, true , false , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000015', 'WBI:100000000000000000000000000000000006', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 2', true , false, true , false , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000016', 'WBI:100000000000000000000000000000000007', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 2', true , false, true , false , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000017', 'WBI:100000000000000000000000000000000008', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 1', true , false, false , false , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000018', 'WBI:100000000000000000000000000000000009', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 1', true , false, true , false , false , false, false, false, false, false, false, false, false, false, false, false, false);
-- Team GPK access
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000019', 'WBI:100000000000000000000000000000000002', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 1' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000020', 'WBI:100000000000000000000000000000000003', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 2' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000019', 'WBI:100000000000000000000000000000000002', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 1', true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000020', 'WBI:100000000000000000000000000000000003', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 2', true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
-- Cross team GPK access
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000021', 'WBI:100000000000000000000000000000000001', 'teamlead-1' , 'Titus Toll' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000022', 'WBI:100000000000000000000000000000000001', 'teamlead-2' , 'Frauke Faul' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000021', 'WBI:100000000000000000000000000000000001', 'teamlead-1' , 'Titus Toll' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000022', 'WBI:100000000000000000000000000000000001', 'teamlead-2' , 'Frauke Faul' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
-- TPK access
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000123', 'WBI:100000000000000000000000000000000010', 'teamlead-1' , 'Titus Toll' , true , false, false , false , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000123', 'WBI:100000000000000000000000000000000010', 'teamlead-1' , 'Titus Toll' , true , false, false , false , false , false, false, false, false, false, false, false, false, false, false, false, false);
-- Access to other domains
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000023', 'WBI:100000000000000000000000000000000012', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 1' , true , false, true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000024', 'WBI:100000000000000000000000000000000013', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 2' , true , false, true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000025', 'WBI:100000000000000000000000000000000014', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 2' , true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000026', 'WBI:100000000000000000000000000000000015', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'KSC 2' , true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000023', 'WBI:100000000000000000000000000000000012', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 1', true , false, true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000024', 'WBI:100000000000000000000000000000000013', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 2', true , false, true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000025', 'WBI:100000000000000000000000000000000014', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 2', true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000026', 'WBI:100000000000000000000000000000000015', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 2', true , true , true , true , false , false, false, false, false, false, false, false, false, false, false, false, false);
-- Access to workbaskets for sorting test
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000900', 'WBI:000000000000000000000000000000000900', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000901', 'WBI:000000000000000000000000000000000901', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000902', 'WBI:000000000000000000000000000000000902', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000903', 'WBI:000000000000000000000000000000000903', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000904', 'WBI:000000000000000000000000000000000904', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000905', 'WBI:000000000000000000000000000000000905', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000906', 'WBI:000000000000000000000000000000000906', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000907', 'WBI:000000000000000000000000000000000907', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000908', 'WBI:000000000000000000000000000000000908', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000909', 'WBI:000000000000000000000000000000000909', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000900', 'WBI:000000000000000000000000000000000900', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000901', 'WBI:000000000000000000000000000000000901', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000902', 'WBI:000000000000000000000000000000000902', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000903', 'WBI:000000000000000000000000000000000903', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000904', 'WBI:000000000000000000000000000000000904', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000905', 'WBI:000000000000000000000000000000000905', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000906', 'WBI:000000000000000000000000000000000906', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000907', 'WBI:000000000000000000000000000000000907', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000908', 'WBI:000000000000000000000000000000000908', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);
INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000909', 'WBI:000000000000000000000000000000000909', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true);

View File

@ -80,6 +80,28 @@
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
</dependency>
<dependency>
<groupId>capital.scalable</groupId>
<artifactId>spring-auto-restdocs-core</artifactId>
<version>${version.auto-restdocs}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${version.hibernate}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<!-- TEST DEPENDENCIES -->

View File

@ -0,0 +1,106 @@
package pro.taskana.common.test;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import capital.scalable.restdocs.AutoDocumentation;
import capital.scalable.restdocs.SnippetRegistry;
import capital.scalable.restdocs.jackson.JacksonResultHandlers;
import capital.scalable.restdocs.response.ResponseModifyingPreprocessors;
import com.fasterxml.jackson.annotation.JsonIgnoreType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Links;
import org.springframework.lang.NonNull;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.restdocs.cli.CliDocumentation;
import org.springframework.restdocs.http.HttpDocumentation;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
import org.springframework.restdocs.operation.preprocess.Preprocessors;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter;
import org.springframework.web.context.WebApplicationContext;
import pro.taskana.common.test.rest.RestHelper;
import pro.taskana.common.test.rest.TaskanaSpringBootTest;
@TaskanaSpringBootTest
@ExtendWith(RestDocumentationExtension.class)
public class BaseRestDocTest {
protected MockMvc mockMvc;
@Autowired protected ObjectMapper objectMapper;
@Autowired protected RestHelper restHelper;
@BeforeEach
public void setUp(
WebApplicationContext webApplicationContext,
RestDocumentationContextProvider restDocumentation) {
objectMapper = objectMapper.addMixIn(Links.class, MixInIgnoreType.class);
this.mockMvc =
MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(springSecurity())
.apply(configureAdminHeadersAsDefault())
.alwaysDo(JacksonResultHandlers.prepareJackson(objectMapper))
.alwaysDo(commonDocumentation())
.apply(
MockMvcRestDocumentation.documentationConfiguration(restDocumentation)
.snippets()
.withDefaults(
CliDocumentation.curlRequest(),
HttpDocumentation.httpRequest(),
HttpDocumentation.httpResponse(),
AutoDocumentation.requestFields().failOnUndocumentedFields(true),
AutoDocumentation.responseFields().failOnUndocumentedFields(true),
AutoDocumentation.pathParameters().failOnUndocumentedParams(true),
AutoDocumentation.requestParameters().failOnUndocumentedParams(true),
AutoDocumentation.description(),
AutoDocumentation.methodAndPath(),
AutoDocumentation.sectionBuilder()
.snippetNames(
SnippetRegistry.AUTO_AUTHORIZATION,
SnippetRegistry.AUTO_PATH_PARAMETERS,
SnippetRegistry.AUTO_REQUEST_PARAMETERS,
SnippetRegistry.AUTO_REQUEST_FIELDS,
SnippetRegistry.AUTO_RESPONSE_FIELDS,
SnippetRegistry.AUTO_LINKS,
SnippetRegistry.HTTP_REQUEST,
SnippetRegistry.HTTP_RESPONSE)
.build()))
.build();
}
protected RestDocumentationResultHandler commonDocumentation(Snippet... snippets) {
return MockMvcRestDocumentation.document(
"{ClassName}/{methodName}",
Preprocessors.preprocessRequest(Preprocessors.prettyPrint()),
Preprocessors.preprocessResponse(
ResponseModifyingPreprocessors.replaceBinaryContent(),
ResponseModifyingPreprocessors.limitJsonArrayLength(objectMapper),
Preprocessors.prettyPrint()),
snippets);
}
private MockMvcConfigurerAdapter configureAdminHeadersAsDefault() {
return new MockMvcConfigurerAdapter() {
@Override
public RequestPostProcessor beforeMockMvcCreated(
@NonNull ConfigurableMockMvcBuilder<?> builder, @NonNull WebApplicationContext cxt) {
builder.defaultRequest(
MockMvcRequestBuilders.post("/test").headers(restHelper.getHeadersAdmin()));
return super.beforeMockMvcCreated(builder, cxt);
}
};
}
@JsonIgnoreType
public static class MixInIgnoreType {}
}

View File

@ -1,50 +0,0 @@
package pro.taskana.common.test.doc.api;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;
import pro.taskana.common.test.doc.api.BaseRestDocumentation.ResultHandlerConfiguration;
import pro.taskana.common.test.rest.RestHelper;
import pro.taskana.common.test.rest.TaskanaSpringBootTest;
/** Base class for Rest Documentation tests. */
@TaskanaSpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(ResultHandlerConfiguration.class)
public abstract class BaseRestDocumentation {
protected static String ADMIN_CREDENTIALS = "Basic YWRtaW46YWRtaW4=";
protected static String TEAMLEAD_1_CREDENTIALS = "Basic dGVhbWxlYWQtMTp0ZWFtbGVhZC0x";
@LocalServerPort protected int port;
@Autowired protected WebApplicationContext context;
@Autowired protected MockMvc mockMvc;
@Autowired protected RestHelper restHelper;
@TestConfiguration
static class ResultHandlerConfiguration {
@Bean
public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() {
return configurer ->
configurer
.operationPreprocessors()
.withRequestDefaults(prettyPrint())
.withResponseDefaults(prettyPrint());
}
}
}

View File

@ -33,18 +33,23 @@ public class RestHelper {
public static final RestTemplate TEMPLATE = getRestTemplate();
private final Environment environment;
private Environment environment;
private int port;
@Autowired
public RestHelper(Environment environment) {
this.environment = environment;
}
public RestHelper(int port) {
this.port = port;
}
public String toUrl(String relativeUrl, Object... uriVariables) {
return UriComponentsBuilder.fromPath(relativeUrl)
.scheme("http")
.host("127.0.0.1")
.port(environment.getProperty("local.server.port"))
.port(getPort())
.build(false)
.expand(uriVariables)
.toString();
@ -103,6 +108,13 @@ public class RestHelper {
return headers;
}
private int getPort() {
if (environment != null) {
return environment.getRequiredProperty("local.server.port", int.class);
}
return port;
}
/**
* Return a REST template which is capable of dealing with responses in HAL format.
*

View File

@ -1,16 +1,13 @@
package pro.taskana.common.rest;
package pro.taskana.common.test.rest;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
@ -18,34 +15,32 @@ import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import pro.taskana.rest.security.SpringSecurityToJaasFilter;
/** Default basic configuration for taskana web example. */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public class TestWebSecurityConfig {
@Value("${taskana.ldap.serverUrl:ldap://localhost:10389}")
private String ldapServerUrl;
private final String ldapServerUrl;
private final String ldapBaseDn;
private final String ldapGroupSearchBase;
private final String ldapGroupSearchFilter;
@Value("${taskana.ldap.baseDn:OU=Test,O=TASKANA}")
private String ldapBaseDn;
@Value("${taskana.ldap.groupSearchBase:cn=groups}")
private String ldapGroupSearchBase;
@Value("${taskana.ldap.userDnPatterns:uid={0},cn=users}")
private String ldapUserDnPatterns;
@Value("${taskana.ldap.groupSearchFilter:uniqueMember={0}}")
private String ldapGroupSearchFilter;
@Autowired
public TestWebSecurityConfig(
@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.groupSearchFilter:uniqueMember={0}}") String ldapGroupSearchFilter) {
this.ldapServerUrl = ldapServerUrl;
this.ldapBaseDn = ldapBaseDn;
this.ldapGroupSearchBase = ldapGroupSearchBase;
this.ldapGroupSearchFilter = ldapGroupSearchFilter;
}
@Bean
public WebMvcConfigurer corsConfigurer() {
@ -60,7 +55,6 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedMethod("POST");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(0);
@ -68,16 +62,13 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
@Bean
public LdapAuthoritiesPopulator authoritiesPopulator() {
public LdapAuthoritiesPopulator authoritiesPopulator(
DefaultSpringSecurityContextSource contextSource) {
Function<Map<String, List<String>>, GrantedAuthority> authorityMapper =
record -> {
String role = record.get("spring.security.ldap.dn").get(0);
return new SimpleGrantedAuthority(role);
};
record -> new SimpleGrantedAuthority(record.get("spring.security.ldap.dn").get(0));
DefaultLdapAuthoritiesPopulator populator =
new DefaultLdapAuthoritiesPopulator(
defaultSpringSecurityContextSource(), ldapGroupSearchBase);
new DefaultLdapAuthoritiesPopulator(contextSource, ldapGroupSearchBase);
populator.setGroupSearchFilter(ldapGroupSearchFilter);
populator.setSearchSubtree(true);
populator.setRolePrefix("");
@ -97,42 +88,6 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
return grantedAuthoritiesMapper;
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userDnPatterns(ldapUserDnPatterns)
.groupSearchBase(ldapGroupSearchBase)
.ldapAuthoritiesPopulator(authoritiesPopulator())
.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;
}
private static class CorsWebMvcConfigurer implements WebMvcConfigurer {
@Override

View File

@ -0,0 +1,11 @@
{{#hasContent}}[%autowidth,width="100%"]
|===
|{{th-path}}|{{th-optional}}|{{th-description}}
{{#content}}
|{{path}}
|{{optional}}
|{{description}}
{{/content}}
|==={{/hasContent}}{{#noContent}}{{no-links}}{{/noContent}}

View File

@ -0,0 +1,12 @@
{{#hasContent}}[%autowidth,width="100%"]
|===
|{{th-parameter}}|{{th-type}}|{{th-optional}}|{{th-description}}
{{#content}}
|{{path}}
|{{type}}
|{{optional}}
|{{description}}
{{/content}}
|==={{/hasContent}}{{#noContent}}{{no-params}}{{/noContent}}

View File

@ -0,0 +1,12 @@
{{#hasContent}}[%autowidth,width="100%"]
|===
|{{th-path}}|{{th-type}}|{{th-optional}}|{{th-description}}
{{#content}}
|{{path}}
|{{type}}
|{{optional}}
|{{description}}
{{/content}}
|==={{/hasContent}}{{#noContent}}{{no-request-body}}{{/noContent}}

View File

@ -0,0 +1,14 @@
{{#isPageRequest}}{{pagination-request-adoc}}
{{/isPageRequest}}{{#hasContent}}[%autowidth,width="100%"]
|===
|{{th-parameter}}|{{th-type}}|{{th-optional}}|{{th-description}}
{{#content}}
|{{path}}
|{{type}}
|{{optional}}
|{{description}}
{{/content}}
|==={{/hasContent}}{{#noContent}}{{no-params}}{{/noContent}}

View File

@ -0,0 +1,14 @@
{{#hasContent}}{{#isPageResponse}}{{pagination-response-adoc}}
{{/isPageResponse}}[%autowidth,width="100%"]
|===
|{{th-path}}|{{th-type}}|{{th-optional}}|{{th-description}}
{{#content}}
|{{path}}
|{{type}}
|{{optional}}
|{{description}}
{{/content}}
|==={{/hasContent}}{{#noContent}}{{no-response-body}}{{/noContent}}

View File

@ -522,6 +522,11 @@ public class TaskHistoryQueryImpl implements TaskHistoryQuery {
return this;
}
@Override
public TaskHistoryQuery orderByTaskHistoryEventId(SortDirection sortDirection) {
return addOrderCriteria("ID", sortDirection);
}
@Override
public TaskHistoryQuery orderByBusinessProcessId(SortDirection sortDirection) {
return addOrderCriteria("BUSINESS_PROCESS_ID", sortDirection);

View File

@ -394,7 +394,7 @@ public interface ClassificationHistoryQuery
* @param sortDirection Determines whether the result is sorted in ascending or descending order.
* If sortDirection is null, the result is sorted in ascending order
* @return the query
* @throws InvalidArgumentException when the number of the custom is incorrect.
* @throws InvalidArgumentException if the number of the custom is incorrect.
*/
ClassificationHistoryQuery orderByCustomAttribute(int num, SortDirection sortDirection)
throws InvalidArgumentException;

View File

@ -335,6 +335,15 @@ public interface TaskHistoryQuery extends BaseQuery<TaskHistoryEvent, TaskHistor
TaskHistoryQuery customAttributeLike(
TaskHistoryCustomField customField, String... searchArguments);
/**
* Sort the query result by the id of the events.
*
* @param sortDirection Determines whether the result is sorted in ascending or descending order.
* If sortDirection is null, the result is sorted in ascending order
* @return the query
*/
TaskHistoryQuery orderByTaskHistoryEventId(SortDirection sortDirection);
/**
* Sort the query result by businessProcessId.
*

View File

@ -306,7 +306,7 @@ public interface WorkbasketHistoryQuery
* @param sortDirection Determines whether the result is sorted in ascending or descending order.
* If sortDirection is null, the result is sorted in ascending order
* @return the query
* @throws InvalidArgumentException when the number of the custom is incorrect.
* @throws InvalidArgumentException if the number of the custom is incorrect.
*/
WorkbasketHistoryQuery orderByCustomAttribute(int num, SortDirection sortDirection)
throws InvalidArgumentException;
@ -318,7 +318,7 @@ public interface WorkbasketHistoryQuery
* @param sortDirection Determines whether the result is sorted in ascending or descending order.
* If sortDirection is null, the result is sorted in ascending order
* @return the query
* @throws InvalidArgumentException when the number of the orgLevel is incorrect.
* @throws InvalidArgumentException if the number of the orgLevel is incorrect.
*/
WorkbasketHistoryQuery orderByOrgLevel(int num, SortDirection sortDirection)
throws InvalidArgumentException;

View File

@ -1,5 +1,6 @@
package acceptance.query;
import static java.lang.String.CASE_INSENSITIVE_ORDER;
import static org.assertj.core.api.Assertions.assertThat;
import acceptance.AbstractAccTest;
@ -109,6 +110,32 @@ class QueryTaskHistoryAccTest extends AbstractAccTest {
assertThat(count).isZero();
}
@Test
void should_SortQueryByIdAsc_When_Requested() {
List<TaskHistoryEvent> events =
getHistoryService()
.createTaskHistoryQuery()
.orderByTaskHistoryEventId(SortDirection.ASCENDING)
.list();
assertThat(events)
.extracting(TaskHistoryEvent::getId)
.isSortedAccordingTo(CASE_INSENSITIVE_ORDER);
}
@Test
void should_SortQueryByIdDesc_When_Requested() {
List<TaskHistoryEvent> events =
getHistoryService()
.createTaskHistoryQuery()
.orderByTaskHistoryEventId(SortDirection.DESCENDING)
.list();
assertThat(events)
.extracting(TaskHistoryEvent::getId)
.isSortedAccordingTo(CASE_INSENSITIVE_ORDER.reversed());
}
@Test
void should_ReturnHistoryEvents_For_DifferentInAttributes() {
List<TaskHistoryEvent> returnValues =

View File

@ -59,6 +59,22 @@
<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>
@ -66,12 +82,17 @@
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<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>
@ -81,28 +102,8 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-core</artifactId>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
@ -110,11 +111,98 @@
<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.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${version.hibernate}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>${version.maven.resources}</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/generated-javadoc-json</outputDirectory>
<resources>
<resource>
<directory>../../rest/taskana-rest-spring/target/generated-javadoc-json
</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<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>compile</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>
@ -126,39 +214,28 @@
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
<attributes>
<snippets>target/generated-snippets</snippets>
<docinfo>shared</docinfo>
</attributes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>${version.maven.resources}</version>
<executions>
<execution>
<id>copy-rest-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/generated-docs
</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/js</directory>
</resource>
</resources>
</configuration>
</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>

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

View File

@ -0,0 +1,21 @@
= TASKANA History module RESTful API Documentation
== Overview
This is the REST documentation for http://taskana.pro)[TASKANA]'s simplehistory 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.
== History event
include::{snippets}/TaskHistoryEventControllerRestDocTest/getAllTaskHistoryEventsDocTest/auto-section.adoc[]
include::{snippets}/TaskHistoryEventControllerRestDocTest/getSpecificTaskHistoryEventDocTest/auto-section.adoc[]

File diff suppressed because one or more lines are too long

View File

@ -1,64 +0,0 @@
<!-- Sourcecode at https://stackoverflow.com/questions/34481638/how-to-use-tocify-with-asciidoctor-for-a-dynamic-toc -->
<script src="./jquery-1.11.3.min.js"></script>
<script src="./jquery-ui.min.js"></script>
<script src="./jquery.tocify.min.js"></script>
<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;
}
</style>
<script type="text/javascript">
$(function () {
$("#toc").append("<div id='generated-toc'></div>");
$("#generated-toc").tocify({
extendPage: true,
context: "#content",
highlightOnScroll: true,
hideEffect: "slideUp",
hashGenerator: function(text, element) {
return $(element).attr("id");
},
smoothScroll: false,
theme: "none",
selectors: $( "#content" ).has( "h1" ).size() > 0 ? "h1,h2,h3,h4,h5" : "h2,h3,h4,h5",
ignoreSelector: ".discrete"
});
var 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>

View File

@ -1,105 +0,0 @@
= Taskana RESTful API Documentation
taskana.pro;
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:
= Overview
[big]#*This Documentation is still under development and probably incomplete and/or flawed in certain areas.*#
== HTTP verbs
The Taskana RESTful API tries to adhere as closely as possible to standard HTTP and REST conventions in its
use of HTTP verbs.
|===
| Verb | Usage
| `GET`
| Used to retrieve a resource
| `POST`
| Used to create a new resource
| `PUT`
| Used to update a resource
| `DELTE`
| Used to delete a existing resource
|===
== HTTP status codes
The Taskana RESTful API tries to adhere as closely as possible to standard HTTP and REST conventions in its
use of HTTP status codes.
|===
| Status code | Usage
| `200 OK`
| The request completed successfully.
| `201 Created`
| The request completed successfully und create the new resource.
| `204 No Content`
| The request completed successfully and there is no content to send in the response payload.
| `400 Bad Request`
| The request was not performed because of a client error like a invalid parameter.
| `401 Unauthorized`
| The request has not been applied because it lacks valid authentication credentials for the target resource.
| `403 FORBIDDEN`
| The current user <user> has no read permission for <Resource Type> <Resource>.
| `404 Not Found`
| The requested resource did not exist.
| `405 Method not allowed`
| The method used in this request is can not be used on this resource.
| `406` Not acceptable
| Wrong content-type in request header.
| `409 Conflict`
| The resource could not be updatet or created because of a conflict with an existing one.
| `415 Unsupported Media Type`
| The content of the request can't be understood due to being in an unsupported media-type.
|===
== Common Fields
Taskana uses Spring HATEOAS to achive the best possible REST-conformity. +
In HATEOAS every response contains a map named *_links* in which the links for navigation are included. +
If a resource has embedded resources these are found in a map named *_embedded*.
== History event
It is a plugin which can get the history of action performed
=== Get all task history event
==== Example request
include::../../../{snippets}/GetAllTaskHistoryEventDocTest/http-request.adoc[]
==== Example response
include::../../../{snippets}/GetAllTaskHistoryEventDocTest/response-body.adoc[]
=== Get a specific task history event
==== Example request
include::../../../{snippets}/GetSpecificTaskHistoryEventDocTest/http-request.adoc[]
==== Example response
include::../../../{snippets}/GetSpecificTaskHistoryEventDocTest/response-body.adoc[]

View File

@ -0,0 +1,11 @@
package pro.taskana.simplehistory.rest;
public class HistoryRestEndpoints {
public static final String API_V1 = "/api/v1/";
public static final String URL_HISTORY_EVENTS = API_V1 + "task-history-event";
public static final String URL_HISTORY_EVENTS_ID = API_V1 + "task-history-event/{historyEventId}";
private HistoryRestEndpoints() {}
}

View File

@ -1,35 +1,31 @@
package pro.taskana.simplehistory.rest;
import java.beans.ConstructorProperties;
import java.sql.SQLException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.List;
import java.util.function.BiConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.PagedModel.PageMetadata;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import pro.taskana.TaskanaEngineConfiguration;
import pro.taskana.common.api.TimeInterval;
import pro.taskana.common.api.BaseQuery.SortDirection;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.rest.AbstractPagingController;
import pro.taskana.common.rest.QueryHelper;
import pro.taskana.common.rest.QueryPagingParameter;
import pro.taskana.common.rest.QuerySortBy;
import pro.taskana.common.rest.QuerySortParameter;
import pro.taskana.simplehistory.impl.SimpleHistoryServiceImpl;
import pro.taskana.simplehistory.impl.task.TaskHistoryQuery;
import pro.taskana.simplehistory.rest.assembler.TaskHistoryEventListResourceAssembler;
import pro.taskana.simplehistory.rest.assembler.TaskHistoryEventRepresentationModelAssembler;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventListResource;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventPagedRepresentationModel;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventRepresentationModel;
import pro.taskana.spi.history.api.events.task.TaskHistoryCustomField;
import pro.taskana.spi.history.api.events.task.TaskHistoryEvent;
@ -38,111 +34,69 @@ import pro.taskana.spi.history.api.exceptions.TaskanaHistoryEventNotFoundExcepti
/** Controller for all TaskHistoryEvent related endpoints. */
@RestController
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
@RequestMapping(path = "/api/v1/task-history-event", produces = "application/hal+json")
public class TaskHistoryEventController extends AbstractPagingController {
public class TaskHistoryEventController {
private static final Logger LOGGER = LoggerFactory.getLogger(TaskHistoryEventController.class);
private static final String LIKE = "%";
private static final String BUSINESS_PROCESS_ID = "business-process-id";
private static final String BUSINESS_PROCESS_ID_LIKE = "business-process-id-like";
private static final String PARENT_BUSINESS_PROCESS_ID = "parent-business-process-id";
private static final String PARENT_BUSINESS_PROCESS_ID_LIKE = "parent-business-process-id-like";
private static final String TASK_ID = "task-id";
private static final String TASK_ID_LIKE = "task-id-like";
private static final String EVENT_TYPE = "event-type";
private static final String EVENT_TYPE_LIKE = "event-type-like";
private static final String CREATED = "created";
private static final String USER_ID = "user-id";
private static final String USER_ID_LIKE = "user-id-like";
private static final String DOMAIN = "domain";
private static final String WORKBASKET_KEY = "workbasket-key";
private static final String WORKBASKET_KEY_LIKE = "workbasket-key-like";
private static final String POR_COMPANY = "por-company";
private static final String POR_COMPANY_LIKE = "por-company-like";
private static final String POR_SYSTEM = "por-system";
private static final String POR_SYSTEM_LIKE = "por-system-like";
private static final String POR_INSTANCE = "por-instance";
private static final String POR_INSTANCE_LIKE = "por-instance-like";
private static final String POR_TYPE = "por-type";
private static final String POR_TYPE_LIKE = "por-type-like";
private static final String POR_VALUE = "por-value";
private static final String POR_VALUE_LIKE = "por-value-like";
private static final String TASK_CLASSIFICATION_KEY = "task-classification-key";
private static final String TASK_CLASSIFICATION_KEY_LIKE = "task-classification-key-like";
private static final String TASK_CLASSIFICATION_CATEGORY = "task-classification-category";
private static final String TASK_CLASSIFICATION_CATEGORY_LIKE =
"task-classification-category-like";
private static final String ATTACHMENT_CLASSIFICATION_KEY = "attachment-classification-key";
private static final String ATTACHMENT_CLASSIFICATION_KEY_LIKE =
"attachment-classification-key-like";
private static final String CUSTOM_1 = "custom-1";
private static final String CUSTOM_2 = "custom-2";
private static final String CUSTOM_3 = "custom-3";
private static final String CUSTOM_4 = "custom-4";
private static final String PAGING_PAGE = "page";
private static final String PAGING_PAGE_SIZE = "page-size";
private final SimpleHistoryServiceImpl simpleHistoryService;
private final TaskHistoryEventRepresentationModelAssembler
taskHistoryEventRepresentationModelAssembler;
private final TaskHistoryEventRepresentationModelAssembler assembler;
@Autowired
public TaskHistoryEventController(
TaskanaEngineConfiguration taskanaEngineConfiguration,
SimpleHistoryServiceImpl simpleHistoryServiceImpl,
TaskHistoryEventRepresentationModelAssembler taskHistoryEventRepresentationModelAssembler)
throws SQLException {
TaskHistoryEventRepresentationModelAssembler assembler) throws SQLException {
this.simpleHistoryService = simpleHistoryServiceImpl;
this.simpleHistoryService.initialize(taskanaEngineConfiguration.buildTaskanaEngine());
this.taskHistoryEventRepresentationModelAssembler =
taskHistoryEventRepresentationModelAssembler;
this.assembler = assembler;
}
@GetMapping
/**
* This endpoint retrieves a list of existing Task History Events. Filters can be applied.
*
* @title Get a list of all Task History Events
* @param filterParameter the filter parameters
* @param sortParameter the sort parameters
* @param pagingParameter the paging parameters
* @return the Task History Events with the given filter, sort and paging options.
*/
@GetMapping(path = HistoryRestEndpoints.URL_HISTORY_EVENTS, produces = MediaTypes.HAL_JSON_VALUE)
@Transactional(readOnly = true, rollbackFor = Exception.class)
public ResponseEntity<TaskHistoryEventListResource> getTaskHistoryEvents(
@RequestParam MultiValueMap<String, String> params) throws InvalidArgumentException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entry to getTaskHistoryEvents(params= {})", params);
}
public ResponseEntity<TaskHistoryEventPagedRepresentationModel> getTaskHistoryEvents(
TaskHistoryQueryFilterParameter filterParameter,
TaskHistoryQuerySortParameter sortParameter,
QueryPagingParameter<TaskHistoryEvent, TaskHistoryQuery> pagingParameter) {
TaskHistoryQuery query = simpleHistoryService.createTaskHistoryQuery();
applySortingParams(query, params);
applyFilterParams(query, params);
filterParameter.applyToQuery(query);
sortParameter.applyToQuery(query);
PageMetadata pageMetadata = null;
List<TaskHistoryEvent> historyEvents;
final String page = params.getFirst(PAGING_PAGE);
final String pageSize = params.getFirst(PAGING_PAGE_SIZE);
params.remove(PAGING_PAGE);
params.remove(PAGING_PAGE_SIZE);
validateNoInvalidParameterIsLeft(params);
if (page != null && pageSize != null) {
long totalElements = query.count();
pageMetadata = initPageMetadata(pageSize, page, totalElements);
historyEvents = query.listPage((int) pageMetadata.getNumber(), (int) pageMetadata.getSize());
} else if (page == null && pageSize == null) {
historyEvents = query.list();
} else {
throw new InvalidArgumentException("Paging information is incomplete.");
}
List<TaskHistoryEvent> historyEvents = pagingParameter.applyToQuery(query);
TaskHistoryEventListResourceAssembler assembler = new TaskHistoryEventListResourceAssembler();
TaskHistoryEventListResource pagedResources =
assembler.toResources(historyEvents, pageMetadata);
TaskHistoryEventPagedRepresentationModel pagedResources =
assembler.toPagedModel(historyEvents, pagingParameter.getPageMetadata());
ResponseEntity<TaskHistoryEventPagedRepresentationModel> response =
ResponseEntity.ok(pagedResources);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"Exit from getTaskHistoryEvents(), returning {}",
new ResponseEntity<>(pagedResources, HttpStatus.OK));
LOGGER.debug("Exit from getTaskHistoryEvents(), returning {}", response);
}
return new ResponseEntity<>(pagedResources, HttpStatus.OK);
return response;
}
@GetMapping(path = "/{historyEventId}", produces = "application/hal+json")
/**
* This endpoint retrieves a single Task History Event.
*
* @title Get a single Task History Event
* @param historyEventId the Id of the requested Task History Event.
* @return the requested Task History Event
* @throws TaskanaHistoryEventNotFoundException If a Task History Event can't be found by the
* provided historyEventId
*/
@GetMapping(path = HistoryRestEndpoints.URL_HISTORY_EVENTS_ID)
@Transactional(readOnly = true, rollbackFor = Exception.class)
public ResponseEntity<TaskHistoryEventRepresentationModel> getTaskHistoryEvent(
@PathVariable String historyEventId) throws TaskanaHistoryEventNotFoundException {
@ -153,8 +107,7 @@ public class TaskHistoryEventController extends AbstractPagingController {
TaskHistoryEvent resultEvent = simpleHistoryService.getTaskHistoryEvent(historyEventId);
TaskHistoryEventRepresentationModel taskEventResource =
taskHistoryEventRepresentationModelAssembler.toModel(resultEvent);
TaskHistoryEventRepresentationModel taskEventResource = assembler.toModel(resultEvent);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
@ -165,272 +118,57 @@ public class TaskHistoryEventController extends AbstractPagingController {
return new ResponseEntity<>(taskEventResource, HttpStatus.OK);
}
private void applySortingParams(TaskHistoryQuery query, MultiValueMap<String, String> params)
throws InvalidArgumentException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entry to applySortingParams(params= {})", params);
public enum TaskHistoryQuerySortBy implements QuerySortBy<TaskHistoryQuery> {
TASK_HISTORY_EVENT_ID(TaskHistoryQuery::orderByTaskHistoryEventId),
BUSINESS_PROCESS_ID(TaskHistoryQuery::orderByBusinessProcessId),
PARENT_BUSINESS_PROCESS_ID(TaskHistoryQuery::orderByParentBusinessProcessId),
TASK_ID(TaskHistoryQuery::orderByTaskId),
EVENT_TYPE(TaskHistoryQuery::orderByEventType),
CREATED(TaskHistoryQuery::orderByCreated),
USER_ID(TaskHistoryQuery::orderByUserId),
DOMAIN(TaskHistoryQuery::orderByDomain),
WORKBASKET_KEY(TaskHistoryQuery::orderByWorkbasketKey),
POR_COMPANY(TaskHistoryQuery::orderByPorCompany),
POR_SYSTEM(TaskHistoryQuery::orderByPorSystem),
POR_INSTANCE(TaskHistoryQuery::orderByPorInstance),
POR_TYPE(TaskHistoryQuery::orderByPorType),
POR_VALUE(TaskHistoryQuery::orderByPorValue),
TASK_CLASSIFICATION_KEY(TaskHistoryQuery::orderByTaskClassificationKey),
TASK_CLASSIFICATION_CATEGORY(TaskHistoryQuery::orderByTaskClassificationCategory),
ATTACHMENT_CLASSIFICATION_KEY(TaskHistoryQuery::orderByAttachmentClassificationKey),
CUSTOM_1((query, sort) -> query.orderByCustomAttribute(TaskHistoryCustomField.CUSTOM_1, sort)),
CUSTOM_2((query, sort) -> query.orderByCustomAttribute(TaskHistoryCustomField.CUSTOM_2, sort)),
CUSTOM_3((query, sort) -> query.orderByCustomAttribute(TaskHistoryCustomField.CUSTOM_3, sort)),
CUSTOM_4((query, sort) -> query.orderByCustomAttribute(TaskHistoryCustomField.CUSTOM_4, sort));
private final BiConsumer<TaskHistoryQuery, SortDirection> consumer;
TaskHistoryQuerySortBy(BiConsumer<TaskHistoryQuery, SortDirection> consumer) {
this.consumer = consumer;
}
QueryHelper.applyAndRemoveSortingParams(
params,
(sortBy, sortDirection) -> {
switch (sortBy) {
case BUSINESS_PROCESS_ID:
query.orderByBusinessProcessId(sortDirection);
break;
case PARENT_BUSINESS_PROCESS_ID:
query.orderByParentBusinessProcessId(sortDirection);
break;
case TASK_ID:
query.orderByTaskId(sortDirection);
break;
case EVENT_TYPE:
query.orderByEventType(sortDirection);
break;
case CREATED:
query.orderByCreated(sortDirection);
break;
case USER_ID:
query.orderByUserId(sortDirection);
break;
case DOMAIN:
query.orderByDomain(sortDirection);
break;
case WORKBASKET_KEY:
query.orderByWorkbasketKey(sortDirection);
break;
case POR_COMPANY:
query.orderByPorCompany(sortDirection);
break;
case POR_SYSTEM:
query.orderByPorSystem(sortDirection);
break;
case POR_INSTANCE:
query.orderByPorInstance(sortDirection);
break;
case POR_TYPE:
query.orderByPorType(sortDirection);
break;
case POR_VALUE:
query.orderByPorValue(sortDirection);
break;
case TASK_CLASSIFICATION_KEY:
query.orderByTaskClassificationKey(sortDirection);
break;
case TASK_CLASSIFICATION_CATEGORY:
query.orderByTaskClassificationCategory(sortDirection);
break;
case ATTACHMENT_CLASSIFICATION_KEY:
query.orderByAttachmentClassificationKey(sortDirection);
break;
case CUSTOM_1:
query.orderByCustomAttribute(TaskHistoryCustomField.CUSTOM_1, sortDirection);
break;
case CUSTOM_2:
query.orderByCustomAttribute(TaskHistoryCustomField.CUSTOM_2, sortDirection);
break;
case CUSTOM_3:
query.orderByCustomAttribute(TaskHistoryCustomField.CUSTOM_3, sortDirection);
break;
case CUSTOM_4:
query.orderByCustomAttribute(TaskHistoryCustomField.CUSTOM_4, sortDirection);
break;
default:
throw new IllegalArgumentException("Unknown order '" + sortBy + "'");
}
});
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exit from applySortingParams(), returning: {}", query);
@Override
public void applySortByForQuery(TaskHistoryQuery query, SortDirection sortDirection) {
consumer.accept(query, sortDirection);
}
}
private void applyFilterParams(TaskHistoryQuery query, MultiValueMap<String, String> params) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entry to applyFilterParams(query= {}, params= {})", query, params);
// Unfortunately this class is necessary, since spring can not inject the generic 'sort-by'
// parameter from the super class.
public static class TaskHistoryQuerySortParameter
extends QuerySortParameter<TaskHistoryQuery, TaskHistoryQuerySortBy> {
@ConstructorProperties({"sort-by", "order"})
public TaskHistoryQuerySortParameter(
List<TaskHistoryQuerySortBy> sortBy, List<SortDirection> order)
throws InvalidArgumentException {
super(sortBy, order);
}
if (params.containsKey(BUSINESS_PROCESS_ID)) {
String[] businessProcessId = extractCommaSeparatedFields(params.get(BUSINESS_PROCESS_ID));
query.businessProcessIdIn(businessProcessId);
params.remove(BUSINESS_PROCESS_ID);
// this getter is necessary for the documentation!
@Override
public List<TaskHistoryQuerySortBy> getSortBy() {
return super.getSortBy();
}
if (params.containsKey(BUSINESS_PROCESS_ID_LIKE)) {
query.businessProcessIdLike(LIKE + params.get(BUSINESS_PROCESS_ID_LIKE).get(0) + LIKE);
params.remove(BUSINESS_PROCESS_ID_LIKE);
}
if (params.containsKey(PARENT_BUSINESS_PROCESS_ID)) {
String[] parentBusinessProcessId =
extractCommaSeparatedFields(params.get(PARENT_BUSINESS_PROCESS_ID));
query.parentBusinessProcessIdIn(parentBusinessProcessId);
params.remove(PARENT_BUSINESS_PROCESS_ID);
}
if (params.containsKey(PARENT_BUSINESS_PROCESS_ID_LIKE)) {
query.parentBusinessProcessIdLike(
LIKE + params.get(PARENT_BUSINESS_PROCESS_ID_LIKE).get(0) + LIKE);
params.remove(PARENT_BUSINESS_PROCESS_ID_LIKE);
}
if (params.containsKey(TASK_ID)) {
String[] taskId = extractCommaSeparatedFields(params.get(TASK_ID));
query.taskIdIn(taskId);
params.remove(TASK_ID);
}
if (params.containsKey(TASK_ID_LIKE)) {
query.taskIdLike(LIKE + params.get(TASK_ID_LIKE).get(0) + LIKE);
params.remove(TASK_ID_LIKE);
}
if (params.containsKey(EVENT_TYPE)) {
String[] eventType = extractCommaSeparatedFields(params.get(EVENT_TYPE));
query.eventTypeIn(eventType);
params.remove(EVENT_TYPE);
}
if (params.containsKey(EVENT_TYPE_LIKE)) {
query.eventTypeLike(LIKE + params.get(EVENT_TYPE_LIKE).get(0) + LIKE);
params.remove(EVENT_TYPE_LIKE);
}
if (params.containsKey(CREATED)) {
String[] created = extractCommaSeparatedFields(params.get(CREATED));
TimeInterval timeInterval = getTimeIntervalOf(created);
query.createdWithin(timeInterval);
params.remove(CREATED);
}
if (params.containsKey(USER_ID)) {
String[] userId = extractCommaSeparatedFields(params.get(USER_ID));
query.userIdIn(userId);
params.remove(USER_ID);
}
if (params.containsKey(USER_ID_LIKE)) {
query.userIdLike(LIKE + params.get(USER_ID_LIKE).get(0) + LIKE);
params.remove(USER_ID_LIKE);
}
if (params.containsKey(DOMAIN)) {
query.domainIn(extractCommaSeparatedFields(params.get(DOMAIN)));
params.remove(DOMAIN);
}
if (params.containsKey(WORKBASKET_KEY)) {
String[] workbasketKey = extractCommaSeparatedFields(params.get(WORKBASKET_KEY));
query.workbasketKeyIn(workbasketKey);
params.remove(WORKBASKET_KEY);
}
if (params.containsKey(WORKBASKET_KEY_LIKE)) {
query.workbasketKeyLike(LIKE + params.get(WORKBASKET_KEY_LIKE).get(0) + LIKE);
params.remove(WORKBASKET_KEY_LIKE);
}
if (params.containsKey(POR_COMPANY)) {
String[] porCompany = extractCommaSeparatedFields(params.get(POR_COMPANY));
query.porCompanyIn(porCompany);
params.remove(POR_COMPANY);
}
if (params.containsKey(POR_COMPANY_LIKE)) {
query.porCompanyLike(LIKE + params.get(POR_COMPANY_LIKE).get(0) + LIKE);
params.remove(POR_COMPANY_LIKE);
}
if (params.containsKey(POR_SYSTEM)) {
String[] porSystem = extractCommaSeparatedFields(params.get(POR_SYSTEM));
query.porSystemIn(porSystem);
params.remove(POR_SYSTEM);
}
if (params.containsKey(POR_SYSTEM_LIKE)) {
query.porSystemLike(LIKE + params.get(POR_SYSTEM_LIKE).get(0) + LIKE);
params.remove(POR_SYSTEM_LIKE);
}
if (params.containsKey(POR_INSTANCE)) {
String[] porInstance = extractCommaSeparatedFields(params.get(POR_INSTANCE));
query.porInstanceIn(porInstance);
params.remove(POR_INSTANCE);
}
if (params.containsKey(POR_INSTANCE_LIKE)) {
query.porInstanceLike(LIKE + params.get(POR_INSTANCE_LIKE).get(0) + LIKE);
params.remove(POR_INSTANCE_LIKE);
}
if (params.containsKey(POR_TYPE)) {
String[] porType = extractCommaSeparatedFields(params.get(POR_TYPE));
query.porTypeIn(porType);
params.remove(POR_TYPE);
}
if (params.containsKey(POR_TYPE_LIKE)) {
query.porTypeLike(LIKE + params.get(POR_TYPE_LIKE).get(0) + LIKE);
params.remove(POR_TYPE_LIKE);
}
if (params.containsKey(POR_VALUE)) {
String[] porValue = extractCommaSeparatedFields(params.get(POR_VALUE));
query.porValueIn(porValue);
params.remove(POR_VALUE);
}
if (params.containsKey(POR_VALUE_LIKE)) {
query.porValueLike(LIKE + params.get(POR_VALUE_LIKE).get(0) + LIKE);
params.remove(POR_VALUE_LIKE);
}
if (params.containsKey(TASK_CLASSIFICATION_KEY)) {
String[] taskClassificationKey =
extractCommaSeparatedFields(params.get(TASK_CLASSIFICATION_KEY));
query.taskClassificationKeyIn(taskClassificationKey);
params.remove(TASK_CLASSIFICATION_KEY);
}
if (params.containsKey(TASK_CLASSIFICATION_KEY_LIKE)) {
query.taskClassificationKeyLike(
LIKE + params.get(TASK_CLASSIFICATION_KEY_LIKE).get(0) + LIKE);
params.remove(TASK_CLASSIFICATION_KEY_LIKE);
}
if (params.containsKey(TASK_CLASSIFICATION_CATEGORY)) {
String[] taskClassificationCategory =
extractCommaSeparatedFields(params.get(TASK_CLASSIFICATION_CATEGORY));
query.taskClassificationCategoryIn(taskClassificationCategory);
params.remove(TASK_CLASSIFICATION_CATEGORY);
}
if (params.containsKey(TASK_CLASSIFICATION_CATEGORY_LIKE)) {
query.taskClassificationCategoryLike(
LIKE + params.get(TASK_CLASSIFICATION_CATEGORY_LIKE).get(0) + LIKE);
params.remove(TASK_CLASSIFICATION_CATEGORY_LIKE);
}
if (params.containsKey(ATTACHMENT_CLASSIFICATION_KEY)) {
String[] attachmentClassificationKey =
extractCommaSeparatedFields(params.get(ATTACHMENT_CLASSIFICATION_KEY));
query.attachmentClassificationKeyIn(attachmentClassificationKey);
params.remove(ATTACHMENT_CLASSIFICATION_KEY);
}
if (params.containsKey(ATTACHMENT_CLASSIFICATION_KEY_LIKE)) {
query.attachmentClassificationKeyLike(
LIKE + params.get(ATTACHMENT_CLASSIFICATION_KEY_LIKE).get(0) + LIKE);
params.remove(ATTACHMENT_CLASSIFICATION_KEY_LIKE);
}
for (TaskHistoryCustomField customField : TaskHistoryCustomField.values()) {
List<String> list = params.remove(customField.name().replace("_", "-").toLowerCase());
if (list != null) {
query.customAttributeIn(customField, extractCommaSeparatedFields(list));
}
list = params.remove(customField.name().replace("_", "-").toLowerCase() + "-like");
if (list != null) {
String[] values = extractCommaSeparatedFields(list);
for (int i = 0; i < values.length; i++) {
values[i] = LIKE + values[i] + LIKE;
}
query.customAttributeLike(customField, values);
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exit from applyFilterParams(), returning {}", query);
}
}
private TimeInterval getTimeIntervalOf(String[] created) {
LocalDate begin;
LocalDate end;
try {
begin = LocalDate.parse(created[0]);
} catch (Exception e) {
throw new IllegalArgumentException(
"Cannot parse String '"
+ created[0]
+ "'. Expected a String of the Format 'yyyy-MM-dd'.");
}
if (created.length < 2) {
end = begin.plusDays(1);
} else {
end = LocalDate.parse(created[1]);
}
Instant beginInst = begin.atStartOfDay(ZoneId.systemDefault()).toInstant();
Instant endInst = end.atStartOfDay(ZoneId.systemDefault()).toInstant();
return new TimeInterval(beginInst, endInst);
}
}

View File

@ -0,0 +1,443 @@
package pro.taskana.simplehistory.rest;
import static java.util.Optional.ofNullable;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.beans.ConstructorProperties;
import java.time.Instant;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.rest.QueryParameter;
import pro.taskana.simplehistory.impl.task.TaskHistoryQuery;
import pro.taskana.spi.history.api.events.task.TaskHistoryCustomField;
public class TaskHistoryQueryFilterParameter implements QueryParameter<TaskHistoryQuery, Void> {
/** Filter by the event type of the Task History Event. This is an exact match. */
@JsonProperty("event-type")
private final String[] eventType;
/**
* Filter by the event type of the Task History Event. This results in a substring search.. (% is
* appended to the beginning and end of the requested value). Further SQL "LIKE" wildcard
* characters will be resolved correctly.
*/
@JsonProperty("event-type-like")
private final String[] eventTypeLike;
/** Filter by the user id of the Task History Event. This is an exact match. */
@JsonProperty("user-id")
private final String[] userId;
/**
* Filter by the user id of the Task History Event. This results in a substring search.. (% is
* appended to the beginning and end of the requested value). Further SQL "LIKE" wildcard
* characters will be resolved correctly.
*/
@JsonProperty("user-id-like")
private final String[] userIdLike;
/**
* Filter by a created time interval. The length of the provided values has to be even. To create
* an open interval you can either use 'null' or just leave it blank.
*
* <p>The format is ISO-8601.
*/
private final Instant[] created;
/** Filter by the domain of the Task History Event. This is an exact match. */
private final String[] domain;
/** Filter by the task id of the Task History Event. This is an exact match. */
@JsonProperty("task-id")
private final String[] taskId;
/**
* Filter by the task id of the Task History Event. This results in a substring search.. (% is
* appended to the beginning and end of the requested value). Further SQL "LIKE" wildcard
* characters will be resolved correctly.
*/
@JsonProperty("task-id-like")
private final String[] taskIdLike;
/** Filter by the business process id of the Task History Event. This is an exact match. */
@JsonProperty("business-process-id")
private final String[] businessProcessId;
/**
* Filter by the business process id of the Task History Event. This results into a substring
* search. (% is appended to the beginning and end of the requested value). Further SQL "LIKE"
* wildcard characters will be resolved correctly.
*/
@JsonProperty("business-process-id-like")
private final String[] businessProcessIdLike;
/** Filter by the parent business process id of the Task History Event. This is an exact match. */
@JsonProperty("parent-business-process-id")
private final String[] parentBusinessProcessId;
/**
* Filter by the parent business process id of the Task History Event. This results into a
* substring search. (% is appended to the beginning and end of the requested value). Further SQL
* "Like" wildcard characters will be resolved correctly.
*/
@JsonProperty("parent-business-process-id-like")
private final String[] parentBusinessProcessIdLike;
/** Filter by the task classification key of the Task History Event. This is an exact match. */
@JsonProperty("task-classification-key")
private final String[] taskClassificationKey;
/**
* Filter by the task classification key of the Task History Event. This results into a substring
* search. (% is appended to the beginning and end of the requested value). Further SQL "LIKE"
* wildcard characters will be resolved correctly.
*/
@JsonProperty("task-classification-key-like")
private final String[] taskClassificationKeyLike;
/**
* Filter by the task classification category of the Task History Event. This is an exact match.
*/
@JsonProperty("task-classification-category")
private final String[] taskClassificationCategory;
/**
* Filter by the task classification category of the Task History Event. This results into a
* substring search. (% is appended to the beginning and end of the requested value). Further SQL
* "Like" wildcard characters will be resolved correctly.
*/
@JsonProperty("task-classification-category-like")
private final String[] taskClassificationCategoryLike;
/**
* Filter by the attachment classification key of the Task History Event. This is an exact match.
*/
@JsonProperty("attachment-classification-key")
private final String[] attachmentClassificationKey;
/**
* Filter by the attachment classification key of the Task History Event. This results into a
* substring search. (% is appended to the beginning and end of the requested value). Further SQL
* "Like" wildcard characters will be resolved correctly.
*/
@JsonProperty("attachment-classification-key-like")
private final String[] attachmentClassificationKeyLike;
/** Filter by the workbasket key of the Task History Event. This is an exact match. */
@JsonProperty("workbasket-key")
private final String[] workbasketKey;
/**
* Filter by the workbasket key of the Task History Event. This results in a substring search.. (%
* is appended to the beginning and end of the requested value). Further SQL "LIKE" wildcard
* characters will be resolved correctly.
*/
@JsonProperty("workbasket-key-like")
private final String[] workbasketKeyLike;
/**
* Filter by the company of the primary object reference of the Task History Event. This is an
* exact match.
*/
@JsonProperty("por-company")
private final String[] porCompany;
/**
* Filter by the company of the primary object reference of the Task History Event. This results
* into a substring search. (% is appended to the beginning and end of the requested value).
* Further SQL "LIKE" wildcard characters will be resolved correctly.
*/
@JsonProperty("por-company-like")
private final String[] porCompanyLike;
/**
* Filter by the system of the primary object reference of the Task History Event. This is an
* exact match.
*/
@JsonProperty("por-system")
private final String[] porSystem;
/**
* Filter by the system of the primary object reference of the Task History Event. This results
* into a substring search. (% is appended to the beginning and end of the requested value).
* Further SQL "LIKE" wildcard characters will be resolved correctly.
*/
@JsonProperty("por-system-like")
private final String[] porSystemLike;
/**
* Filter by the system instance of the primary object reference of the Task History Event. This
* is an exact match.
*/
@JsonProperty("por-instance")
private final String[] porInstance;
/**
* Filter by the system instance of the primary object reference of the Task History Event. This
* results into a substring search. (% is appended to the beginning and end of the requested
* value). Further SQL "LIKE" wildcard characters will be resolved correctly.
*/
@JsonProperty("por-instance-like")
private final String[] porInstanceLike;
/**
* Filter by the value of the primary object reference of the Task History Event. This is an exact
* match.
*/
@JsonProperty("por-value")
private final String[] porValue;
/**
* Filter by the value of the primary object reference of the Task History Event. This results
* into a substring search. (% is appended to the beginning and end of the requested value).
* Further SQL "LIKE" wildcard characters will be resolved correctly.
*/
@JsonProperty("por-value-like")
private final String[] porValueLike;
/** Filter by the value of the field custom1. This is an exact match. */
@JsonProperty("custom-1")
private final String[] custom1;
/**
* Filter by the value of the field custom1. This is an exact match. This results into a substring
* search. (% is appended to the beginning and end of the requested value). Further SQL "LIKE"
* wildcard characters will be resolved correctly.
*/
@JsonProperty("custom-1-like")
private final String[] custom1Like;
/** Filter by the value of the field custom2. This is an exact match. */
@JsonProperty("custom-2")
private final String[] custom2;
/**
* Filter by the value of the field custom2. This is an exact match. This results into a substring
* search. (% is appended to the beginning and end of the requested value). Further SQL "LIKE"
* wildcard characters will be resolved correctly.
*/
@JsonProperty("custom-2-like")
private final String[] custom2Like;
/** Filter by the value of the field custom3. This is an exact match. */
@JsonProperty("custom-3")
private final String[] custom3;
/**
* Filter by the value of the field custom3. This is an exact match. This results into a substring
* search. (% is appended to the beginning and end of the requested value). Further SQL "LIKE"
* wildcard characters will be resolved correctly.
*/
@JsonProperty("custom-3-like")
private final String[] custom3Like;
/** Filter by the value of the field custom4. This is an exact match. */
@JsonProperty("custom-4")
private final String[] custom4;
/**
* Filter by the value of the field custom4. This is an exact match. This results into a substring
* search. (% is appended to the beginning and end of the requested value). Further SQL "LIKE"
* wildcard characters will be resolved correctly.
*/
@JsonProperty("custom-4-like")
private final String[] custom4Like;
@SuppressWarnings("indentation")
@ConstructorProperties({
"event-type",
"event-type-like",
"user-id",
"user-id-like",
"created",
"domain",
"task-id",
"task-id-like",
"business-process-id",
"business-process-id-like",
"parent-business-process-id",
"parent-business-process-id-like",
"task-classification-key",
"task-classification-key-like",
"task-classification-category",
"task-classification-category-like",
"attachment-classification-key",
"attachment-classification-key-like",
"workbasket-key",
"workbasket-key-like",
"por-company",
"por-company-like",
"por-system",
"por-system-like",
"por-instance",
"por-instance-like",
"por-value",
"por-value-like",
"custom-1",
"custom-1-like",
"custom-2",
"custom-2-like",
"custom-3",
"custom-3-like",
"custom-4",
"custom-4-like",
})
public TaskHistoryQueryFilterParameter(
String[] eventType,
String[] eventTypeLike,
String[] userId,
String[] userIdLike,
Instant[] created,
String[] domain,
String[] taskId,
String[] taskIdLike,
String[] businessProcessId,
String[] businessProcessIdLike,
String[] parentBusinessProcessId,
String[] parentBusinessProcessIdLike,
String[] taskClassificationKey,
String[] taskClassificationKeyLike,
String[] taskClassificationCategory,
String[] taskClassificationCategoryLike,
String[] attachmentClassificationKey,
String[] attachmentClassificationKeyLike,
String[] workbasketKey,
String[] workbasketKeyLike,
String[] porCompany,
String[] porCompanyLike,
String[] porSystem,
String[] porSystemLike,
String[] porInstance,
String[] porInstanceLike,
String[] porValue,
String[] porValueLike,
String[] custom1,
String[] custom1Like,
String[] custom2,
String[] custom2Like,
String[] custom3,
String[] custom3Like,
String[] custom4,
String[] custom4Like)
throws InvalidArgumentException {
this.eventType = eventType;
this.eventTypeLike = eventTypeLike;
this.userId = userId;
this.userIdLike = userIdLike;
this.created = created;
this.domain = domain;
this.taskId = taskId;
this.taskIdLike = taskIdLike;
this.businessProcessId = businessProcessId;
this.businessProcessIdLike = businessProcessIdLike;
this.parentBusinessProcessId = parentBusinessProcessId;
this.parentBusinessProcessIdLike = parentBusinessProcessIdLike;
this.taskClassificationKey = taskClassificationKey;
this.taskClassificationKeyLike = taskClassificationKeyLike;
this.taskClassificationCategory = taskClassificationCategory;
this.taskClassificationCategoryLike = taskClassificationCategoryLike;
this.attachmentClassificationKey = attachmentClassificationKey;
this.attachmentClassificationKeyLike = attachmentClassificationKeyLike;
this.workbasketKey = workbasketKey;
this.workbasketKeyLike = workbasketKeyLike;
this.porCompany = porCompany;
this.porCompanyLike = porCompanyLike;
this.porSystem = porSystem;
this.porSystemLike = porSystemLike;
this.porInstance = porInstance;
this.porInstanceLike = porInstanceLike;
this.porValue = porValue;
this.porValueLike = porValueLike;
this.custom1 = custom1;
this.custom1Like = custom1Like;
this.custom2 = custom2;
this.custom2Like = custom2Like;
this.custom3 = custom3;
this.custom3Like = custom3Like;
this.custom4 = custom4;
this.custom4Like = custom4Like;
validateFilterParameters();
}
@Override
public Void applyToQuery(TaskHistoryQuery query) {
ofNullable(eventType).ifPresent(query::eventTypeIn);
ofNullable(eventTypeLike)
.map(this::wrapElementsInLikeStatement)
.ifPresent(query::eventTypeLike);
ofNullable(userId).ifPresent(query::userIdIn);
ofNullable(userIdLike).map(this::wrapElementsInLikeStatement).ifPresent(query::userIdLike);
ofNullable(created).map(this::extractTimeIntervals).ifPresent(query::createdWithin);
ofNullable(domain).ifPresent(query::domainIn);
ofNullable(taskId).ifPresent(query::taskIdIn);
ofNullable(taskIdLike).map(this::wrapElementsInLikeStatement).ifPresent(query::taskIdLike);
ofNullable(businessProcessId).ifPresent(query::businessProcessIdIn);
ofNullable(businessProcessIdLike)
.map(this::wrapElementsInLikeStatement)
.ifPresent(query::businessProcessIdLike);
ofNullable(parentBusinessProcessId).ifPresent(query::parentBusinessProcessIdIn);
ofNullable(parentBusinessProcessIdLike)
.map(this::wrapElementsInLikeStatement)
.ifPresent(query::parentBusinessProcessIdLike);
ofNullable(taskClassificationKey).ifPresent(query::taskClassificationKeyIn);
ofNullable(taskClassificationKeyLike)
.map(this::wrapElementsInLikeStatement)
.ifPresent(query::taskClassificationKeyLike);
ofNullable(taskClassificationCategory).ifPresent(query::taskClassificationCategoryIn);
ofNullable(taskClassificationCategoryLike)
.map(this::wrapElementsInLikeStatement)
.ifPresent(query::taskClassificationCategoryLike);
ofNullable(attachmentClassificationKey).ifPresent(query::attachmentClassificationKeyIn);
ofNullable(attachmentClassificationKeyLike)
.map(this::wrapElementsInLikeStatement)
.ifPresent(query::attachmentClassificationKeyLike);
ofNullable(workbasketKey).ifPresent(query::workbasketKeyIn);
ofNullable(workbasketKeyLike)
.map(this::wrapElementsInLikeStatement)
.ifPresent(query::workbasketKeyLike);
ofNullable(porCompany).ifPresent(query::porCompanyIn);
ofNullable(porCompanyLike)
.map(this::wrapElementsInLikeStatement)
.ifPresent(query::porCompanyLike);
ofNullable(porSystem).ifPresent(query::porSystemIn);
ofNullable(porSystemLike)
.map(this::wrapElementsInLikeStatement)
.ifPresent(query::porSystemLike);
ofNullable(porInstance).ifPresent(query::porInstanceIn);
ofNullable(porInstanceLike)
.map(this::wrapElementsInLikeStatement)
.ifPresent(query::porInstanceLike);
ofNullable(porValue).ifPresent(query::porValueIn);
ofNullable(porValueLike).map(this::wrapElementsInLikeStatement).ifPresent(query::porValueLike);
ofNullable(custom1)
.ifPresent(arr -> query.customAttributeIn(TaskHistoryCustomField.CUSTOM_1, arr));
ofNullable(custom1Like)
.map(this::wrapElementsInLikeStatement)
.ifPresent(arr -> query.customAttributeLike(TaskHistoryCustomField.CUSTOM_1, arr));
ofNullable(custom2)
.ifPresent(arr -> query.customAttributeIn(TaskHistoryCustomField.CUSTOM_2, arr));
ofNullable(custom2Like)
.map(this::wrapElementsInLikeStatement)
.ifPresent(arr -> query.customAttributeLike(TaskHistoryCustomField.CUSTOM_2, arr));
ofNullable(custom3)
.ifPresent(arr -> query.customAttributeIn(TaskHistoryCustomField.CUSTOM_3, arr));
ofNullable(custom3Like)
.map(this::wrapElementsInLikeStatement)
.ifPresent(arr -> query.customAttributeLike(TaskHistoryCustomField.CUSTOM_3, arr));
ofNullable(custom4)
.ifPresent(arr -> query.customAttributeIn(TaskHistoryCustomField.CUSTOM_4, arr));
ofNullable(custom4Like)
.map(this::wrapElementsInLikeStatement)
.ifPresent(arr -> query.customAttributeLike(TaskHistoryCustomField.CUSTOM_4, arr));
return null;
}
private void validateFilterParameters() throws InvalidArgumentException {
if (created != null && created.length % 2 != 0) {
throw new InvalidArgumentException(
"provided length of the property 'created' is not dividable by 2");
}
}
}

View File

@ -1,85 +0,0 @@
package pro.taskana.simplehistory.rest.assembler;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.PagedModel.PageMetadata;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponentsBuilder;
import pro.taskana.simplehistory.rest.TaskHistoryEventController;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventListResource;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventRepresentationModel;
import pro.taskana.spi.history.api.events.task.TaskHistoryEvent;
/** Mapper to convert from a list of TaskHistoryEvent to a TaskHistoryEventResource. */
public class TaskHistoryEventListResourceAssembler {
public TaskHistoryEventListResource toResources(
List<TaskHistoryEvent> historyEvents, PageMetadata pageMetadata) {
TaskHistoryEventRepresentationModelAssembler assembler =
new TaskHistoryEventRepresentationModelAssembler();
List<TaskHistoryEventRepresentationModel> resources =
new ArrayList<>(assembler.toCollectionModel(historyEvents).getContent());
TaskHistoryEventListResource pagedResources =
new TaskHistoryEventListResource(resources, pageMetadata);
pagedResources.add(Link.of(getBaseUri().toUriString()).withSelfRel());
if (pageMetadata != null) {
pagedResources.add(linkTo(TaskHistoryEventController.class).withRel("allTaskHistoryEvent"));
pagedResources.add(
Link.of(getBaseUri().replaceQueryParam("page", 1).toUriString())
.withRel(IanaLinkRelations.FIRST));
pagedResources.add(
Link.of(
getBaseUri()
.replaceQueryParam("page", pageMetadata.getTotalPages())
.toUriString())
.withRel(IanaLinkRelations.LAST));
if (pageMetadata.getNumber() > 1) {
pagedResources.add(
Link.of(
getBaseUri()
.replaceQueryParam("page", pageMetadata.getNumber() - 1)
.toUriString())
.withRel(IanaLinkRelations.PREV));
}
if (pageMetadata.getNumber() < pageMetadata.getTotalPages()) {
pagedResources.add(
Link.of(
getBaseUri()
.replaceQueryParam("page", pageMetadata.getNumber() + 1)
.toUriString())
.withRel(IanaLinkRelations.NEXT));
}
}
return pagedResources;
}
private UriComponentsBuilder getBaseUri() {
HttpServletRequest request =
((ServletRequestAttributes)
Objects.requireNonNull(RequestContextHolder.getRequestAttributes()))
.getRequest();
UriComponentsBuilder baseUri =
ServletUriComponentsBuilder.fromServletMapping(request).path(request.getRequestURI());
for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
for (String value : entry.getValue()) {
baseUri.queryParam(entry.getKey(), value);
}
}
return baseUri;
}
}

View File

@ -3,20 +3,25 @@ package pro.taskana.simplehistory.rest.assembler;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
import org.springframework.hateoas.server.RepresentationModelAssembler;
import java.util.Collection;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import pro.taskana.common.api.exceptions.SystemException;
import pro.taskana.common.rest.assembler.PagedRepresentationModelAssembler;
import pro.taskana.common.rest.models.PageMetadata;
import pro.taskana.simplehistory.rest.TaskHistoryEventController;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventPagedRepresentationModel;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventRepresentationModel;
import pro.taskana.spi.history.api.events.task.TaskHistoryCustomField;
import pro.taskana.spi.history.api.events.task.TaskHistoryEvent;
/** Transforms any {@link TaskHistoryEvent} into its {@link TaskHistoryEventRepresentationModel}. */
@Component
public class TaskHistoryEventRepresentationModelAssembler
implements RepresentationModelAssembler<TaskHistoryEvent, TaskHistoryEventRepresentationModel> {
implements PagedRepresentationModelAssembler<
TaskHistoryEvent,
TaskHistoryEventRepresentationModel,
TaskHistoryEventPagedRepresentationModel> {
@NonNull
@Override
@ -53,8 +58,14 @@ public class TaskHistoryEventRepresentationModelAssembler
.getTaskHistoryEvent(historyEvent.getId()))
.withSelfRel());
} catch (Exception e) {
throw new SystemException("caught unexpecte Exception", e);
throw new SystemException("caught unexpected Exception", e);
}
return repModel;
}
@Override
public TaskHistoryEventPagedRepresentationModel buildPageableEntity(
Collection<TaskHistoryEventRepresentationModel> content, PageMetadata pageMetadata) {
return new TaskHistoryEventPagedRepresentationModel(content, pageMetadata);
}
}

View File

@ -1,29 +0,0 @@
package pro.taskana.simplehistory.rest.models;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Collection;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.PagedModel.PageMetadata;
import pro.taskana.common.rest.models.PagedResources;
/** Resource class for {@link TaskHistoryEventRepresentationModel} with Pagination. */
public class TaskHistoryEventListResource
extends PagedResources<TaskHistoryEventRepresentationModel> {
@SuppressWarnings("unused")
private TaskHistoryEventListResource() {}
public TaskHistoryEventListResource(
Collection<TaskHistoryEventRepresentationModel> content,
PageMetadata metadata,
Link... links) {
super(content, metadata, links);
}
@Override
@JsonProperty("taskHistoryEvents")
public Collection<TaskHistoryEventRepresentationModel> getContent() {
return super.getContent();
}
}

View File

@ -0,0 +1,25 @@
package pro.taskana.simplehistory.rest.models;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.beans.ConstructorProperties;
import java.util.Collection;
import pro.taskana.common.rest.models.PageMetadata;
import pro.taskana.common.rest.models.PagedRepresentationModel;
public class TaskHistoryEventPagedRepresentationModel
extends PagedRepresentationModel<TaskHistoryEventRepresentationModel> {
@ConstructorProperties({"taskHistoryEvents", "page"})
public TaskHistoryEventPagedRepresentationModel(
Collection<TaskHistoryEventRepresentationModel> content, PageMetadata pageMetadata) {
super(content, pageMetadata);
}
/** the embedded task history events. */
@JsonProperty("taskHistoryEvents")
@Override
public Collection<TaskHistoryEventRepresentationModel> getContent() {
return super.getContent();
}
}

View File

@ -9,29 +9,57 @@ import pro.taskana.spi.history.api.events.task.TaskHistoryEvent;
public class TaskHistoryEventRepresentationModel
extends RepresentationModel<TaskHistoryEventRepresentationModel> {
/** Unique Id. */
private String taskHistoryId;
/** The Id of the business process. */
private String businessProcessId;
/** The Id of the parent business process. */
private String parentBusinessProcessId;
/** The Id of the task. */
private String taskId;
/** The type of the event. */
private String eventType;
/**
* The time of event creation.
*
* <p>The format is ISO-8601.
*/
private Instant created;
/** The Id of the user. */
private String userId;
/** Domain. */
private String domain;
/** The key of the Workbasket. */
private String workbasketKey;
/** The company the referenced primary object belongs to. */
private String porCompany;
/** The type of the referenced primary object (contract, claim, policy, customer, ...). */
private String porType;
/** The (kind of) system, the referenced primary object resides in (e.g. SAP, MySystem A, ...). */
private String porSystem;
/** The instance of the system where the referenced primary object is located. */
private String porInstance;
/** The value of the primary object reference. */
private String porValue;
/** The key of the task's classification. */
private String taskClassificationKey;
/** The category of the task's classification. */
private String taskClassificationCategory;
/** The classification key of the task's attachment. */
private String attachmentClassificationKey;
/** The old value. */
private String oldValue;
/** The new value. */
private String newValue;
/** A custom property with name "1". */
private String custom1;
/** A custom property with name "2". */
private String custom2;
/** A custom property with name "3". */
private String custom3;
/** A custom property with name "4". */
private String custom4;
/** details of changes within the task. */
private String details;
public String getTaskHistoryId() {

View File

@ -1,151 +0,0 @@
package pro.taskana.doc.api;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
import java.util.HashMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import pro.taskana.common.test.doc.api.BaseRestDocumentation;
/** Generate documentation for the history event controller. */
class TaskHistoryEventControllerRestDocumentation extends BaseRestDocumentation {
private final HashMap<String, String> taskHistoryEventFieldDescriptionsMap = new HashMap<>();
private FieldDescriptor[] allTaskHistoryEventFieldDescriptors;
private FieldDescriptor[] taskHistoryEventFieldDescriptors;
@BeforeEach
void setUp() {
taskHistoryEventFieldDescriptionsMap.put("taskHistoryId", "Unique ID");
taskHistoryEventFieldDescriptionsMap.put("businessProcessId", "The id of the business process");
taskHistoryEventFieldDescriptionsMap.put(
"parentBusinessProcessId", "The id of the parent business process");
taskHistoryEventFieldDescriptionsMap.put("taskId", "The id of the task");
taskHistoryEventFieldDescriptionsMap.put("eventType", "The type of the event");
taskHistoryEventFieldDescriptionsMap.put("created", "The time was created");
taskHistoryEventFieldDescriptionsMap.put("userId", "The id of the user");
taskHistoryEventFieldDescriptionsMap.put("domain", "Domain");
taskHistoryEventFieldDescriptionsMap.put("workbasketKey", "The key of workbasket");
taskHistoryEventFieldDescriptionsMap.put("porCompany", "");
taskHistoryEventFieldDescriptionsMap.put("porSystem", "");
taskHistoryEventFieldDescriptionsMap.put("porInstance", "");
taskHistoryEventFieldDescriptionsMap.put("porValue", "");
taskHistoryEventFieldDescriptionsMap.put("porType", "");
taskHistoryEventFieldDescriptionsMap.put(
"taskClassificationKey", "The key of classification task");
taskHistoryEventFieldDescriptionsMap.put(
"taskClassificationCategory", "The category of classification");
taskHistoryEventFieldDescriptionsMap.put("attachmentClassificationKey", "");
taskHistoryEventFieldDescriptionsMap.put("oldValue", "The old value");
taskHistoryEventFieldDescriptionsMap.put("newValue", "The new value");
taskHistoryEventFieldDescriptionsMap.put("custom1", "A custom property with name \"1\"");
taskHistoryEventFieldDescriptionsMap.put("custom2", "A custom property with name \"2\"");
taskHistoryEventFieldDescriptionsMap.put("custom3", "A custom property with name \"3\"");
taskHistoryEventFieldDescriptionsMap.put("custom4", "A custom property with name \"4\"");
taskHistoryEventFieldDescriptionsMap.put("details", "details of changes within the task");
taskHistoryEventFieldDescriptionsMap.put(
"_links.self.href", "The links of this task history event");
taskHistoryEventFieldDescriptionsMap.put(
"_links.allTaskHistoryEvent.href", "Link to all task history event");
taskHistoryEventFieldDescriptionsMap.put("_links.first.href", "Link to the first result");
taskHistoryEventFieldDescriptionsMap.put("_links.last.href", "Link to the last result");
allTaskHistoryEventFieldDescriptors =
new FieldDescriptor[] {
subsectionWithPath("taskHistoryEvents").description("An array of Task history event"),
fieldWithPath("_links.allTaskHistoryEvent.href").ignored(),
fieldWithPath("_links.self.href").ignored(),
fieldWithPath("_links.first.href").ignored(),
fieldWithPath("_links.last.href").ignored(),
fieldWithPath("_links.next.href").ignored(),
fieldWithPath("page.size").ignored(),
fieldWithPath("page.totalElements").ignored(),
fieldWithPath("page.totalPages").ignored(),
fieldWithPath("page.number").ignored()
};
taskHistoryEventFieldDescriptors =
new FieldDescriptor[] {
fieldWithPath("taskHistoryId")
.description(taskHistoryEventFieldDescriptionsMap.get("taskHistoryId")),
fieldWithPath("businessProcessId")
.description(taskHistoryEventFieldDescriptionsMap.get("businessProcessId")),
fieldWithPath("parentBusinessProcessId")
.description(taskHistoryEventFieldDescriptionsMap.get("parentBusinessProcessId")),
fieldWithPath("taskId").description(taskHistoryEventFieldDescriptionsMap.get("taskId")),
fieldWithPath("eventType")
.description(taskHistoryEventFieldDescriptionsMap.get("eventType")),
fieldWithPath("created").description(taskHistoryEventFieldDescriptionsMap.get("created")),
fieldWithPath("userId").description(taskHistoryEventFieldDescriptionsMap.get("userId")),
fieldWithPath("domain").description(taskHistoryEventFieldDescriptionsMap.get("domain")),
fieldWithPath("workbasketKey")
.description(taskHistoryEventFieldDescriptionsMap.get("workbasketKey")),
fieldWithPath("porCompany")
.description(taskHistoryEventFieldDescriptionsMap.get("porCompany")),
fieldWithPath("porSystem")
.description(taskHistoryEventFieldDescriptionsMap.get("porSystem")),
fieldWithPath("porInstance")
.description(taskHistoryEventFieldDescriptionsMap.get("porInstance")),
fieldWithPath("porValue")
.description(taskHistoryEventFieldDescriptionsMap.get("porValue")),
fieldWithPath("porType").description(taskHistoryEventFieldDescriptionsMap.get("porType")),
fieldWithPath("taskClassificationKey")
.description(taskHistoryEventFieldDescriptionsMap.get("taskClassificationKey")),
fieldWithPath("taskClassificationCategory")
.description(taskHistoryEventFieldDescriptionsMap.get("taskClassificationCategory")),
fieldWithPath("attachmentClassificationKey")
.description(taskHistoryEventFieldDescriptionsMap.get("attachmentClassificationKey")),
fieldWithPath("oldValue")
.description(taskHistoryEventFieldDescriptionsMap.get("oldValue")),
fieldWithPath("newValue")
.description(taskHistoryEventFieldDescriptionsMap.get("newValue")),
fieldWithPath("custom1").description(taskHistoryEventFieldDescriptionsMap.get("custom1")),
fieldWithPath("custom2").description(taskHistoryEventFieldDescriptionsMap.get("custom2")),
fieldWithPath("custom3").description(taskHistoryEventFieldDescriptionsMap.get("custom3")),
fieldWithPath("custom4").description(taskHistoryEventFieldDescriptionsMap.get("custom4")),
fieldWithPath("details").description(taskHistoryEventFieldDescriptionsMap.get("details")),
fieldWithPath("_links.self.href").ignored()
};
}
@Test
void getAllTaskHistoryEventDocTest() throws Exception {
this.mockMvc
.perform(
RestDocumentationRequestBuilders.get(
"http://127.0.0.1:" + port + "/api/v1/task-history-event?page=1&page-size=3")
.accept("application/hal+json")
.header("Authorization", TEAMLEAD_1_CREDENTIALS))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(
MockMvcRestDocumentation.document(
"GetAllTaskHistoryEventDocTest",
responseFields(allTaskHistoryEventFieldDescriptors)));
}
@Test
void getSpecificTaskHistoryEventDocTest() throws Exception {
this.mockMvc
.perform(
RestDocumentationRequestBuilders.get(
"http://127.0.0.1:"
+ port
+ "/api/v1/task-history-event/THI:000000000000000000000000000000000000")
.accept("application/hal+json")
.header("Authorization", TEAMLEAD_1_CREDENTIALS))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(
MockMvcRestDocumentation.document(
"GetSpecificTaskHistoryEventDocTest",
responseFields(taskHistoryEventFieldDescriptors)));
}
}

View File

@ -0,0 +1,79 @@
package pro.taskana.simplehistory.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
// this class is copied from taskana-rest-spring.
// We can't move it to taskana-common-test because we use the SpringSecurityToJaasFilter
// which is declared in taskana-rest-spring
public class HistoryWebSecurityConfigurer 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 HistoryWebSecurityConfigurer(
@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

@ -1,10 +1,15 @@
package pro.taskana.simplehistory.rest;
import static java.lang.String.CASE_INSENSITIVE_ORDER;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static pro.taskana.common.test.rest.RestHelper.TEMPLATE;
import java.time.LocalDateTime;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
@ -14,19 +19,18 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import pro.taskana.common.rest.models.PageMetadata;
import pro.taskana.common.test.rest.RestHelper;
import pro.taskana.common.test.rest.TaskanaSpringBootTest;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventListResource;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventPagedRepresentationModel;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventRepresentationModel;
/** Controller for integration test. */
@TaskanaSpringBootTest
class TaskHistoryEventControllerIntTest {
private static final RestTemplate TEMPLATE = RestHelper.TEMPLATE;
private final RestHelper restHelper;
@Autowired
@ -34,80 +38,95 @@ class TaskHistoryEventControllerIntTest {
this.restHelper = restHelper;
}
// region Get Task History Events
@Test
void testGetAllHistoryEvent() {
ResponseEntity<TaskHistoryEventListResource> response =
void should_GetAllHistoryEvents_When_UrlIsVisited() {
ResponseEntity<TaskHistoryEventPagedRepresentationModel> response =
TEMPLATE.exchange(
restHelper.toUrl("/api/v1/task-history-event"),
restHelper.toUrl(HistoryRestEndpoints.URL_HISTORY_EVENTS),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskHistoryEventListResource.class));
ParameterizedTypeReference.forType(TaskHistoryEventPagedRepresentationModel.class));
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getLink(IanaLinkRelations.SELF)).isNotNull();
assertThat(response.getBody().getContent()).hasSize(45);
}
@Test
void testGetAllHistoryEventDescendingOrder() {
String url =
"/api/v1/task-history-event?sort-by=business-process-id&order=desc&page-size=3&page=1";
ResponseEntity<TaskHistoryEventListResource> response =
void should_GenerateSelfLink_When_TaskHistoryEventsAreRequested() {
ResponseEntity<TaskHistoryEventPagedRepresentationModel> response =
TEMPLATE.exchange(
restHelper.toUrl(url),
restHelper.toUrl(HistoryRestEndpoints.URL_HISTORY_EVENTS),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskHistoryEventListResource.class));
ParameterizedTypeReference.forType(TaskHistoryEventPagedRepresentationModel.class));
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getContent()).hasSize(3);
assertThat(response.getBody().getLink(IanaLinkRelations.SELF))
.isNotNull()
.isPresent()
.get()
.extracting(Link::getHref)
.asString()
.endsWith(url);
.endsWith(HistoryRestEndpoints.URL_HISTORY_EVENTS);
}
@Test
void should_ReturnSpecificTaskHistoryEventWithoutDetails_When_ListIsQueried() {
String url =
"/api/v1/task-history-event?business-process-id=BPI:01"
+ "&sort-by=business-process-id&order=asc&page-size=6&page=1";
ResponseEntity<TaskHistoryEventListResource> response =
void should_ContainQueryParametersInComputedSelfLink_When_TaskHistoryEventsAreRequested() {
String parameters = "?domain=DOMAIN_A&domain=DOMAIN_B";
ResponseEntity<TaskHistoryEventPagedRepresentationModel> response =
TEMPLATE.exchange(
restHelper.toUrl(url),
restHelper.toUrl(HistoryRestEndpoints.URL_HISTORY_EVENTS + parameters),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskHistoryEventListResource.class));
ParameterizedTypeReference.forType(TaskHistoryEventPagedRepresentationModel.class));
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getLink(IanaLinkRelations.SELF)).isNotNull();
assertThat(response.getBody().getMetadata()).isNotNull();
assertThat(response.getBody().getContent()).hasSize(1);
assertThat(response.getBody().getContent().iterator().next().getDetails()).isNull();
assertThat(response.getBody().getLink(IanaLinkRelations.SELF))
.isPresent()
.get()
.extracting(Link::getHref)
.asString()
.endsWith(HistoryRestEndpoints.URL_HISTORY_EVENTS + parameters);
}
@Test
void should_ReturnSpecificTaskHistoryEventWithDetails_When_SingleEventIsQueried() {
ResponseEntity<TaskHistoryEventRepresentationModel> response =
void should_SortEventsByBusinessProcessIdDesc_When_SortByAndOrderQueryParametersAreDeclared() {
String parameters = "?sort-by=BUSINESS_PROCESS_ID&order=DESCENDING";
ResponseEntity<TaskHistoryEventPagedRepresentationModel> response =
TEMPLATE.exchange(
restHelper.toUrl("/api/v1/task-history-event/THI:000000000000000000000000000000000000"),
restHelper.toUrl(HistoryRestEndpoints.URL_HISTORY_EVENTS + parameters),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskHistoryEventRepresentationModel.class));
ParameterizedTypeReference.forType(TaskHistoryEventPagedRepresentationModel.class));
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getLink(IanaLinkRelations.SELF)).isNotNull();
assertThat(response.getBody().getDetails()).isNotNull();
assertThat(response.getBody().getContent())
.extracting(TaskHistoryEventRepresentationModel::getBusinessProcessId)
.isSortedAccordingTo(CASE_INSENSITIVE_ORDER.reversed());
}
@Test
void testThrowsExceptionIfInvalidFilterIsUsed() {
void should_ApplyBusinessProcessIdFilter_When_QueryParameterIsProvided() {
String parameters = "?business-process-id=BPI:01";
ResponseEntity<TaskHistoryEventPagedRepresentationModel> response =
TEMPLATE.exchange(
restHelper.toUrl(HistoryRestEndpoints.URL_HISTORY_EVENTS + parameters),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskHistoryEventPagedRepresentationModel.class));
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getContent())
.extracting(TaskHistoryEventRepresentationModel::getTaskHistoryId)
.containsExactly("THI:000000000000000000000000000000000000");
}
@Test
@Disabled("no solution for this")
void should_ReturnBadStatusErrorCode_When_InvalidQueryParameterIsUsed() {
ThrowingCallable httpCall =
() ->
TEMPLATE.exchange(
restHelper.toUrl("/api/v1/task-history-event?invalid=BPI:01"),
restHelper.toUrl(HistoryRestEndpoints.URL_HISTORY_EVENTS + "?invalid=BPI:01"),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskHistoryEventListResource.class));
ParameterizedTypeReference.forType(TaskHistoryEventPagedRepresentationModel.class));
assertThatThrownBy(httpCall)
.isInstanceOf(HttpClientErrorException.class)
.hasMessageContaining("[invalid]")
@ -116,63 +135,132 @@ class TaskHistoryEventControllerIntTest {
}
@Test
void testGetHistoryEventOfDate() {
String currentTime = LocalDateTime.now().toString();
final String finalCurrentTime = currentTime;
@Disabled("Jörg pls fix this")
void should_ReturnBadStatusErrorCode_When_CreatedQueryParameterIsWrongFormatted() {
String currentTime = "wrong format";
ThrowingCallable httpCall =
() ->
TEMPLATE.exchange(
restHelper.toUrl("/api/v1/task-history-event?created=" + finalCurrentTime),
restHelper.toUrl(
HistoryRestEndpoints.URL_HISTORY_EVENTS + "?created=" + currentTime),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskHistoryEventListResource.class));
ParameterizedTypeReference.forType(TaskHistoryEventPagedRepresentationModel.class));
assertThatThrownBy(httpCall)
.isInstanceOf(HttpClientErrorException.class)
.hasMessageContaining(currentTime)
.extracting(ex -> ((HttpClientErrorException) ex).getStatusCode())
.isEqualTo(HttpStatus.BAD_REQUEST);
}
// correct Format 'yyyy-MM-dd'
currentTime = currentTime.substring(0, 10);
ResponseEntity<TaskHistoryEventListResource> response =
@Test
void should_ApplyCreatedFilter_When_QueryParametersAreProvided() {
Instant now = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant();
ResponseEntity<TaskHistoryEventPagedRepresentationModel> response =
TEMPLATE.exchange(
restHelper.toUrl("/api/v1/task-history-event?created=" + currentTime),
restHelper.toUrl(
HistoryRestEndpoints.URL_HISTORY_EVENTS + "?created=" + now + "&created="),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskHistoryEventListResource.class));
ParameterizedTypeReference.forType(TaskHistoryEventPagedRepresentationModel.class));
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getLink(IanaLinkRelations.SELF)).isNotNull();
assertThat(response.getBody().getContent()).hasSize(23);
}
@Test
void testGetSecondPageSortedByKey() {
String url = "/api/v1/task-history-event?sort-by=workbasket-key&order=desc&page=2&page-size=2";
ResponseEntity<TaskHistoryEventListResource> response =
void should_ApplyPaging_When_PagingIsRequested() {
String expectedFirstPageParameters = "?sort-by=TASK_HISTORY_EVENT_ID&page-size=3&page=1";
String expectedLastPageParameters = "?sort-by=TASK_HISTORY_EVENT_ID&page-size=3&page=15";
String expectedPrevPageParameters = "?sort-by=TASK_HISTORY_EVENT_ID&page-size=3&page=2";
String expectedNextPageParameters = "?sort-by=TASK_HISTORY_EVENT_ID&page-size=3&page=4";
String parameters = "?sort-by=TASK_HISTORY_EVENT_ID&page-size=3&page=3";
ResponseEntity<TaskHistoryEventPagedRepresentationModel> response =
TEMPLATE.exchange(
restHelper.toUrl(url),
restHelper.toUrl(HistoryRestEndpoints.URL_HISTORY_EVENTS + parameters),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskHistoryEventListResource.class));
ParameterizedTypeReference.forType(TaskHistoryEventPagedRepresentationModel.class));
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getContent()).hasSize(2);
assertThat(response.getBody().getContent().iterator().next().getWorkbasketKey())
.isEqualTo("WBI:100000000000000000000000000000000002");
assertThat(response.getBody().getLink(IanaLinkRelations.SELF))
.isNotNull()
.get()
.extracting(Link::getHref)
.asString()
.endsWith(url);
assertThat(response.getBody().getLink("allTaskHistoryEvent"))
.isNotNull()
.get()
.extracting(Link::getHref)
.asString()
.endsWith("/api/v1/task-history-event");
assertThat(response.getBody().getContent())
.extracting(TaskHistoryEventRepresentationModel::getTaskHistoryId)
.containsExactly(
"THI:000000000000000000000000000000000006",
"THI:000000000000000000000000000000000007",
"THI:000000000000000000000000000000000008");
assertThat(response.getBody().getLink(IanaLinkRelations.FIRST)).isNotNull();
assertThat(response.getBody().getLink(IanaLinkRelations.LAST)).isNotNull();
assertThat(response.getBody().getPageMetadata()).isEqualTo(new PageMetadata(3, 45, 15, 3));
assertThat(response.getBody().getLink(IanaLinkRelations.FIRST))
.isPresent()
.get()
.extracting(Link::getHref)
.asString()
.endsWith(HistoryRestEndpoints.URL_HISTORY_EVENTS + expectedFirstPageParameters);
assertThat(response.getBody().getLink(IanaLinkRelations.LAST))
.isPresent()
.get()
.extracting(Link::getHref)
.asString()
.endsWith(HistoryRestEndpoints.URL_HISTORY_EVENTS + expectedLastPageParameters);
assertThat(response.getBody().getLink(IanaLinkRelations.PREV))
.isPresent()
.get()
.extracting(Link::getHref)
.asString()
.endsWith(HistoryRestEndpoints.URL_HISTORY_EVENTS + expectedPrevPageParameters);
assertThat(response.getBody().getLink(IanaLinkRelations.NEXT))
.isPresent()
.get()
.extracting(Link::getHref)
.asString()
.endsWith(HistoryRestEndpoints.URL_HISTORY_EVENTS + expectedNextPageParameters);
}
// endregion
// region Get Specific Task History Event
@Test
void should_GenerateSelfLink_When_SpecificTaskHistoryEventIsRequested() {
String id = "THI:000000000000000000000000000000000000";
String expectedUrl =
UriComponentsBuilder.fromPath(HistoryRestEndpoints.URL_HISTORY_EVENTS_ID)
.buildAndExpand(id)
.toUriString();
ResponseEntity<TaskHistoryEventPagedRepresentationModel> response =
TEMPLATE.exchange(
restHelper.toUrl(HistoryRestEndpoints.URL_HISTORY_EVENTS_ID, id),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskHistoryEventPagedRepresentationModel.class));
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getLink(IanaLinkRelations.SELF))
.isPresent()
.get()
.extracting(Link::getHref)
.asString()
.endsWith(expectedUrl);
}
@Test
void should_GetSpecificTaskHistoryEventWithDetails_When_SingleEventIsQueried() {
ResponseEntity<TaskHistoryEventRepresentationModel> response =
TEMPLATE.exchange(
restHelper.toUrl(
HistoryRestEndpoints.URL_HISTORY_EVENTS_ID,
"THI:000000000000000000000000000000000000"),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskHistoryEventRepresentationModel.class));
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getDetails()).isNotNull();
}
// endregion
}

View File

@ -0,0 +1,28 @@
package pro.taskana.simplehistory.rest;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import pro.taskana.common.test.BaseRestDocTest;
class TaskHistoryEventControllerRestDocTest extends BaseRestDocTest {
@Test
void getAllTaskHistoryEventsDocTest() throws Exception {
mockMvc
.perform(get(HistoryRestEndpoints.URL_HISTORY_EVENTS + "?page=1&page-size=3"))
.andExpect(MockMvcResultMatchers.status().isOk());
}
@Test
void getSpecificTaskHistoryEventDocTest() throws Exception {
mockMvc
.perform(
get(
HistoryRestEndpoints.URL_HISTORY_EVENTS_ID,
"THI:000000000000000000000000000000000000"))
.andExpect(MockMvcResultMatchers.status().isOk());
}
}

View File

@ -162,7 +162,7 @@ public interface ClassificationQuery
* @param customField identifies which custom attribute is affected.
* @param searchArguments the customField values of the searched for tasks
* @return the query
* @throws InvalidArgumentException when searchArguments is empty or null
* @throws InvalidArgumentException if searchArguments is empty or null
*/
ClassificationQuery customAttributeIn(
ClassificationCustomField customField, String... searchArguments)
@ -176,7 +176,7 @@ public interface ClassificationQuery
* @param customField identifies which custom attribute is affected.
* @param searchArguments the customField values of the searched-for tasks
* @return the query
* @throws InvalidArgumentException when searchArguments is empty or null
* @throws InvalidArgumentException if searchArguments is empty or null
*/
ClassificationQuery customAttributeLike(
ClassificationCustomField customField, String... searchArguments)

View File

@ -71,7 +71,7 @@ public interface ClassificationService {
*
* @param classification the classification to insert
* @return classification which is persisted with unique ID.
* @throws ClassificationAlreadyExistException when the classification does already exists at the
* @throws ClassificationAlreadyExistException if the classification does already exists at the
* given domain.
* @throws NotAuthorizedException if the current user is not member of role BUSINESS_ADMIN or
* ADMIN
@ -88,10 +88,10 @@ public interface ClassificationService {
*
* @param classification the Classification to update
* @return the updated Classification.
* @throws ClassificationNotFoundException when the classification OR it´s parent does not exist.
* @throws NotAuthorizedException when the caller got no ADMIN or BUSINESS_ADMIN permissions.
* @throws ConcurrencyException when the Classification was modified meanwhile and is not latest
* anymore.
* @throws ClassificationNotFoundException if the classification OR it´s parent does not exist.
* @throws NotAuthorizedException if the caller got no ADMIN or BUSINESS_ADMIN permissions.
* @throws ConcurrencyException If the classification was modified in the meantime and is not the
* most up to date anymore.
* @throws InvalidArgumentException if the ServiceLevel property does not comply with the ISO 8601
* specification
*/

View File

@ -4,7 +4,7 @@ import pro.taskana.classification.api.ClassificationCustomField;
/**
* Interface for ClassificationSummaries. This is a specific short model-object which only requieres
* the most important informations. Specific ones can be load afterwards via ID.
* the most important information. Specific ones can be load afterwards via ID.
*/
public interface ClassificationSummary {

View File

@ -4,7 +4,6 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.api.exceptions.NotAuthorizedException;
import pro.taskana.monitor.api.reports.header.TaskStatusColumnHeader;
import pro.taskana.monitor.api.reports.item.TaskQueryItem;
@ -29,7 +28,7 @@ public class TaskStatusReport extends Report<TaskQueryItem, TaskStatusColumnHead
public interface Builder extends Report.Builder<TaskQueryItem, TaskStatusColumnHeader> {
@Override
TaskStatusReport buildReport() throws NotAuthorizedException, InvalidArgumentException;
TaskStatusReport buildReport() throws NotAuthorizedException;
/**
* Adds a list of states to the builder. The created report contains only tasks with a state in

View File

@ -133,7 +133,7 @@ public interface TimeIntervalReportBuilder<
* @param timestamp The task timestamp of interest
* @return The build report
* @throws NotAuthorizedException if the user has no rights to access the monitor
* @throws InvalidArgumentException when an error occurs
* @throws InvalidArgumentException if an error occurs
*/
Report<I, H> buildReport(TaskTimestamp timestamp)
throws NotAuthorizedException, InvalidArgumentException;

View File

@ -447,7 +447,7 @@ public interface TaskQuery extends BaseQuery<TaskSummary, TaskQueryColumnName> {
* @param customField identifies which custom attribute is affected.
* @param searchArguments the customField values of the searched for tasks
* @return the query
* @throws InvalidArgumentException when searchArguments is not given
* @throws InvalidArgumentException if searchArguments are not given
*/
TaskQuery customAttributeIn(TaskCustomField customField, String... searchArguments)
throws InvalidArgumentException;

View File

@ -62,8 +62,8 @@ public interface TaskService {
* @param taskId id of the task which should be unclaimed.
* @return updated unclaimed task
* @throws TaskNotFoundException if the task can´t be found or does not exist
* @throws InvalidStateException when the task is already completed.
* @throws InvalidOwnerException when the task is claimed by another user.
* @throws InvalidStateException if the task is already in an end state.
* @throws InvalidOwnerException if the task is claimed by another user.
* @throws NotAuthorizedException if the current user has no read permission for the workbasket
* the task is in
*/
@ -77,9 +77,8 @@ public interface TaskService {
* @param taskId id of the task which should be unclaimed.
* @return updated unclaimed task
* @throws TaskNotFoundException if the task can´t be found or does not exist
* @throws InvalidStateException when the task is already completed.
* @throws InvalidOwnerException when forceCancel is false and the task is claimed by another
* user.
* @throws InvalidStateException if the task is already in an end state.
* @throws InvalidOwnerException if forceCancel is false and the task is claimed by another user.
* @throws NotAuthorizedException if the current user has no read permission for the workbasket
* the task is in
*/
@ -93,7 +92,7 @@ public interface TaskService {
*
* @param taskId - Id of the Task which should be completed.
* @return Task - updated task after completion.
* @throws InvalidStateException when Task wasn´t claimed before.
* @throws InvalidStateException if Task wasn´t claimed before.
* @throws TaskNotFoundException if the given Task can´t be found in DB.
* @throws InvalidOwnerException if current user is not the task-owner or administrator.
* @throws NotAuthorizedException if the current user has no read permission for the workbasket
@ -109,7 +108,7 @@ public interface TaskService {
*
* @param taskId - Id of the Task which should be completed.
* @return Task - updated task after completion.
* @throws InvalidStateException when Task wasn´t claimed before.
* @throws InvalidStateException if Task wasn´t claimed before.
* @throws TaskNotFoundException if the given Task can´t be found in DB.
* @throws InvalidOwnerException if current user is not the task-owner or administrator.
* @throws NotAuthorizedException if the current user has no read permission for the workbasket
@ -124,9 +123,9 @@ public interface TaskService {
*
* @param taskToCreate the transient task object to be persisted
* @return the created and persisted task
* @throws TaskAlreadyExistException when the Task does already exist.
* @throws TaskAlreadyExistException if the Task does already exist.
* @throws NotAuthorizedException thrown if the current user is not authorized to create that task
* @throws WorkbasketNotFoundException thrown if the work basket referenced by the task is not
* @throws WorkbasketNotFoundException thrown if the workbasket referenced by the task is not
* found
* @throws ClassificationNotFoundException thrown if the {@link Classification} referenced by the
* task is not found
@ -307,7 +306,7 @@ public interface TaskService {
*
* @param taskId The Id of the task to delete.
* @throws TaskNotFoundException If the given Id does not refer to an existing task.
* @throws InvalidStateException If the state of the referenced task is not Completed.
* @throws InvalidStateException If the state of the referenced task is not an end state.
* @throws NotAuthorizedException if the current user is not member of role ADMIN
*/
void deleteTask(String taskId)
@ -318,8 +317,8 @@ public interface TaskService {
*
* @param taskId The Id of the task to delete.
* @throws TaskNotFoundException If the given Id does not refer to an existing task.
* @throws InvalidStateException If the state of the referenced task is not Completed and
* forceDelet is false.
* @throws InvalidStateException If the state of the referenced task is not an end state and
* forceDelete is false.
* @throws NotAuthorizedException if the current user is not member of role ADMIN
*/
void forceDeleteTask(String taskId)

View File

@ -146,7 +146,7 @@ public interface WorkbasketQuery extends BaseQuery<WorkbasketSummary, Workbasket
* @param permission which should be used for results.
* @param accessIds Users which sould be checked for given permissions on workbaskets.
* @return the current query object.
* @throws InvalidArgumentException when permission OR the accessIds are NULL.
* @throws InvalidArgumentException if permission OR the accessIds are NULL.
* @throws NotAuthorizedException if the current user is not member of role BUSINESS_ADMIN or
* ADMIN
*/

View File

@ -90,7 +90,7 @@ public interface WorkbasketService {
*
* @param workbasketAccessItem the new workbasketAccessItem
* @return the created WorkbasketAccessItem
* @throws InvalidArgumentException when the preconditions dont match the required ones.
* @throws InvalidArgumentException if the preconditions dont match the required ones.
* @throws NotAuthorizedException if the current user is not member of role BUSINESS_ADMIN or
* ADMIN
* @throws WorkbasketNotFoundException if the workbasketAccessItem refers to a not existing
@ -279,8 +279,8 @@ public interface WorkbasketService {
throws NotAuthorizedException, WorkbasketNotFoundException;
/**
* Remove a distribution target from a workbasket. If the the specified distribution target
* doesn't exist, the method silently returns without doing anything.
* Remove a distribution target from a workbasket. If the specified distribution target doesn't
* exist, the method silently returns without doing anything.
*
* @param sourceWorkbasketId The id of the source workbasket
* @param targetWorkbasketId The id of the target workbasket

View File

@ -16,10 +16,6 @@ import pro.taskana.task.api.models.TaskSummary;
@ExtendWith(JaasExtension.class)
class TaskQueryAccTest extends AbstractAccTest {
TaskQueryAccTest() {
super();
}
@Test
void testTaskQueryUnauthenticated() {
TaskService taskService = taskanaEngine.getTaskService();

View File

@ -37,7 +37,7 @@ class CancelTaskAccTest extends AbstractAccTest {
@WithAccessId(user = "user-1-2")
@Test
void testQeryCancelledTasks() {
void testQueryCancelledTasks() {
List<TaskSummary> taskSummaries =
taskService.createTaskQuery().stateIn(TaskState.CANCELLED).list();
assertThat(taskSummaries).hasSize(5);

View File

@ -18,7 +18,7 @@ import pro.taskana.workbasket.api.WorkbasketType;
import pro.taskana.workbasket.internal.models.WorkbasketAccessItemImpl;
import pro.taskana.workbasket.internal.models.WorkbasketImpl;
// TODO: this has to be moved to taskana-common but this is currently not possible due to the the
// TODO: this has to be moved to taskana-common but this is currently not possible due to the
// workbasket and task imports
class ObjectAttributeChangeDetectorTest {

View File

@ -68,6 +68,8 @@
<version.openpojo>0.8.13</version.openpojo>
<version.jacoco>0.8.6</version.jacoco>
<version.slf4j-test>1.2.0</version.slf4j-test>
<version.auto-restdocs>2.0.9</version.auto-restdocs>
<version.hibernate>6.1.6.Final</version.hibernate>
<!-- database driver versions -->
<version.db2>11.1.1.1</version.db2>
@ -313,11 +315,6 @@
<version>${version.maven.surefire}</version>
<configuration>
<argLine>${argLine}</argLine>
<!-- Required for generation of REST documentation -->
<includes>
<include>**/*Test.java</include>
<include>**/*Documentation.java</include>
</includes>
</configuration>
</plugin>
<plugin>

View File

@ -50,6 +50,36 @@
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>${version.maven.resources}</version>
<executions>
<execution>
<id>copy-history-rest-documentation-to-static-folder</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.outputDirectory}/static/docs/rest
</outputDirectory>
<resources>
<resource>
<directory>
../../history/taskana-simplehistory-rest-spring/target/generated-docs
</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>historyLogging.plugin</id>

View File

@ -1,29 +1,16 @@
package pro.taskana.rest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
import pro.taskana.sampledata.SampleDataGenerator;
/** Example Application showing a minimal implementation of the taskana REST service. */
@EnableScheduling
@EnableAutoConfiguration
@SpringBootApplication
@ComponentScan("pro.taskana")
public class ExampleRestApplication {
@Autowired
public ExampleRestApplication(
SampleDataGenerator sampleDataGenerator,
@Value("${generateSampleData:true}") boolean generateSampleData) {
if (generateSampleData) {
sampleDataGenerator.generateSampleData();
}
}
public static void main(String[] args) {
SpringApplication.run(ExampleRestApplication.class, args);
}

View File

@ -3,7 +3,6 @@ package pro.taskana.rest;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.h2.tools.Server;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -16,13 +15,6 @@ import pro.taskana.sampledata.SampleDataGenerator;
@Configuration
public class ExampleRestConfiguration {
private final String schemaName;
@Autowired
public ExampleRestConfiguration(@Value("${taskana.schemaName:TASKANA}") String schemaName) {
this.schemaName = schemaName;
}
@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
@ -30,8 +22,15 @@ public class ExampleRestConfiguration {
@Bean
@DependsOn("getTaskanaEngine") // generate sample data after schema was inserted
public SampleDataGenerator generateSampleData(DataSource dataSource) {
return new SampleDataGenerator(dataSource, schemaName);
public SampleDataGenerator generateSampleData(
DataSource dataSource,
@Value("${taskana.schemaName:TASKANA}") String schemaName,
@Value("${generateSampleData:true}") boolean generateSampleData) {
SampleDataGenerator sampleDataGenerator = new SampleDataGenerator(dataSource, schemaName);
if (generateSampleData) {
sampleDataGenerator.generateSampleData();
}
return sampleDataGenerator;
}
// only required to let the adapter example connect to the same database

View File

@ -0,0 +1,117 @@
package pro.taskana.rest.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
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 org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import pro.taskana.common.rest.SpringSecurityToJaasFilter;
/** Default basic configuration for taskana web example. */
@EnableWebSecurity
public class BootWebSecurityConfigurer 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;
private final boolean devMode;
public BootWebSecurityConfigurer(
@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,
@Value("${devMode:false}") boolean devMode) {
this.ldapAuthoritiesPopulator = ldapAuthoritiesPopulator;
this.grantedAuthoritiesMapper = grantedAuthoritiesMapper;
this.ldapServerUrl = ldapServerUrl;
this.ldapBaseDn = ldapBaseDn;
this.ldapGroupSearchBase = ldapGroupSearchBase;
this.ldapUserDnPatterns = ldapUserDnPatterns;
this.devMode = devMode;
}
@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()
.antMatchers("/css/**", "/img/**")
.permitAll()
.and()
.csrf()
.disable()
.httpBasic()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/docs/**")
.permitAll()
.and()
.addFilter(jaasApiIntegrationFilter())
.addFilterAfter(new SpringSecurityToJaasFilter(), JaasApiIntegrationFilter.class);
if (devMode) {
http.headers()
.frameOptions()
.sameOrigin()
.and()
.authorizeRequests()
.antMatchers("/h2-console/**")
.permitAll();
} else {
addLoginPageConfiguration(http);
}
}
protected void addLoginPageConfiguration(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.fullyAuthenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.defaultSuccessUrl("/")
.permitAll()
.and()
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout")
.deleteCookies("JSESSIONID")
.permitAll();
}
protected JaasApiIntegrationFilter jaasApiIntegrationFilter() {
JaasApiIntegrationFilter filter = new JaasApiIntegrationFilter();
filter.setCreateEmptySubject(true);
return filter;
}
}

View File

@ -1,180 +0,0 @@
package pro.taskana.rest.security;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/** Default basic configuration for taskana web example. */
@Configuration
@EnableWebSecurity
public class SpringBootWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${taskana.ldap.serverUrl:ldap://localhost:10389}")
private String ldapServerUrl;
@Value("${taskana.ldap.baseDn:OU=Test,O=TASKANA}")
private String ldapBaseDn;
@Value("${taskana.ldap.groupSearchBase:cn=groups}")
private String ldapGroupSearchBase;
@Value("${taskana.ldap.userDnPatterns:uid={0},cn=users}")
private String ldapUserDnPatterns;
@Value("${taskana.ldap.groupSearchFilter:uniqueMember={0}}")
private String ldapGroupSearchFilter;
@Value("${devMode:false}")
private boolean devMode;
@Bean
public WebMvcConfigurer corsConfigurer() {
return new CorsWebMvcConfigurer();
}
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
@Bean
public DefaultSpringSecurityContextSource defaultSpringSecurityContextSource() {
return new DefaultSpringSecurityContextSource(ldapServerUrl + "/" + ldapBaseDn);
}
@Bean
public LdapAuthoritiesPopulator authoritiesPopulator() {
Function<Map<String, List<String>>, GrantedAuthority> authorityMapper =
record -> {
String role = record.get("spring.security.ldap.dn").get(0);
return new SimpleGrantedAuthority(role);
};
DefaultLdapAuthoritiesPopulator populator =
new DefaultLdapAuthoritiesPopulator(
defaultSpringSecurityContextSource(), ldapGroupSearchBase);
populator.setGroupSearchFilter(ldapGroupSearchFilter);
populator.setSearchSubtree(true);
populator.setRolePrefix("");
populator.setAuthorityMapper(authorityMapper);
return populator;
}
@Bean
public GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
SimpleAuthorityMapper grantedAuthoritiesMapper = new SimpleAuthorityMapper();
grantedAuthoritiesMapper.setPrefix("");
return grantedAuthoritiesMapper;
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userDnPatterns(ldapUserDnPatterns)
.groupSearchBase(ldapGroupSearchBase)
.ldapAuthoritiesPopulator(authoritiesPopulator())
.authoritiesMapper(grantedAuthoritiesMapper())
.contextSource()
.url(ldapServerUrl + "/" + ldapBaseDn)
.and()
.passwordCompare()
.passwordAttribute("userPassword");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/css/**", "/img/**")
.permitAll()
.and()
.csrf()
.disable()
.httpBasic()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/docs/**")
.permitAll()
.and()
.addFilter(jaasApiIntegrationFilter())
.addFilterAfter(new SpringSecurityToJaasFilter(), JaasApiIntegrationFilter.class);
if (devMode) {
http.headers()
.frameOptions()
.sameOrigin()
.and()
.authorizeRequests()
.antMatchers("/h2-console/**")
.permitAll();
} else {
addLoginPageConfiguration(http);
}
}
protected void addLoginPageConfiguration(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.fullyAuthenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.defaultSuccessUrl("/")
.permitAll()
.and()
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout")
.deleteCookies("JSESSIONID")
.permitAll();
}
protected JaasApiIntegrationFilter jaasApiIntegrationFilter() {
JaasApiIntegrationFilter filter = new JaasApiIntegrationFilter();
filter.setCreateEmptySubject(true);
return filter;
}
private static class CorsWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*");
}
}
}

View File

@ -0,0 +1,97 @@
package pro.taskana.rest;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class ExampleWebSecurityConfig {
private final String ldapServerUrl;
private final String ldapBaseDn;
private final String ldapGroupSearchBase;
private final String ldapGroupSearchFilter;
@Autowired
public ExampleWebSecurityConfig(
@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.groupSearchFilter:uniqueMember={0}}") String ldapGroupSearchFilter) {
this.ldapServerUrl = ldapServerUrl;
this.ldapBaseDn = ldapBaseDn;
this.ldapGroupSearchBase = ldapGroupSearchBase;
this.ldapGroupSearchFilter = ldapGroupSearchFilter;
}
@Bean
public WebMvcConfigurer corsConfigurer() {
return new CorsWebMvcConfigurer();
}
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
@Bean
public LdapAuthoritiesPopulator authoritiesPopulator(
DefaultSpringSecurityContextSource contextSource) {
Function<Map<String, List<String>>, GrantedAuthority> authorityMapper =
record -> new SimpleGrantedAuthority(record.get("spring.security.ldap.dn").get(0));
DefaultLdapAuthoritiesPopulator populator =
new DefaultLdapAuthoritiesPopulator(contextSource, ldapGroupSearchBase);
populator.setGroupSearchFilter(ldapGroupSearchFilter);
populator.setSearchSubtree(true);
populator.setRolePrefix("");
populator.setAuthorityMapper(authorityMapper);
return populator;
}
@Bean
public DefaultSpringSecurityContextSource defaultSpringSecurityContextSource() {
return new DefaultSpringSecurityContextSource(ldapServerUrl + "/" + ldapBaseDn);
}
@Bean
public GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
SimpleAuthorityMapper grantedAuthoritiesMapper = new SimpleAuthorityMapper();
grantedAuthoritiesMapper.setPrefix("");
return grantedAuthoritiesMapper;
}
private static class CorsWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*");
}
}
}

View File

@ -22,7 +22,12 @@ public class WebMvcConfig implements WebMvcConfigurer {
"classpath:/static/", "classpath:/public/"
};
@Autowired private ObjectMapper objectMapper;
private final ObjectMapper objectMapper;
@Autowired
public WebMvcConfig(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {

View File

@ -0,0 +1,76 @@
package pro.taskana;
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 CommonWebSecurityConfigurer 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 CommonWebSecurityConfigurer(
@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

@ -1,181 +0,0 @@
package pro.taskana.rest.security;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/** Default basic configuration for taskana web example. */
@Configuration
@EnableWebSecurity
public class SpringBootWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${taskana.ldap.serverUrl:ldap://localhost:10389}")
private String ldapServerUrl;
@Value("${taskana.ldap.baseDn:OU=Test,O=TASKANA}")
private String ldapBaseDn;
@Value("${taskana.ldap.groupSearchBase:cn=groups}")
private String ldapGroupSearchBase;
@Value("${taskana.ldap.userDnPatterns:uid={0},cn=users}")
private String ldapUserDnPatterns;
@Value("${taskana.ldap.groupSearchFilter:uniqueMember={0}}")
private String ldapGroupSearchFilter;
@Value("${devMode:false}")
private boolean devMode;
@Bean
public WebMvcConfigurer corsConfigurer() {
return new CorsWebMvcConfigurer();
}
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedMethod("POST");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
@Bean
public LdapAuthoritiesPopulator authoritiesPopulator() {
Function<Map<String, List<String>>, GrantedAuthority> authorityMapper =
record -> {
String role = record.get("spring.security.ldap.dn").get(0);
return new SimpleGrantedAuthority(role);
};
DefaultLdapAuthoritiesPopulator populator =
new DefaultLdapAuthoritiesPopulator(
defaultSpringSecurityContextSource(), ldapGroupSearchBase);
populator.setGroupSearchFilter(ldapGroupSearchFilter);
populator.setSearchSubtree(true);
populator.setRolePrefix("");
populator.setAuthorityMapper(authorityMapper);
return populator;
}
@Bean
public DefaultSpringSecurityContextSource defaultSpringSecurityContextSource() {
return new DefaultSpringSecurityContextSource(ldapServerUrl + "/" + ldapBaseDn);
}
@Bean
public GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
SimpleAuthorityMapper grantedAuthoritiesMapper = new SimpleAuthorityMapper();
grantedAuthoritiesMapper.setPrefix("");
return grantedAuthoritiesMapper;
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userDnPatterns(ldapUserDnPatterns)
.groupSearchBase(ldapGroupSearchBase)
.ldapAuthoritiesPopulator(authoritiesPopulator())
.authoritiesMapper(grantedAuthoritiesMapper())
.contextSource()
.url(ldapServerUrl + "/" + ldapBaseDn)
.and()
.passwordCompare()
.passwordAttribute("userPassword");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/css/**", "/img/**")
.permitAll()
.and()
.csrf()
.disable()
.httpBasic()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/docs/**")
.permitAll()
.and()
.addFilter(jaasApiIntegrationFilter())
.addFilterAfter(new SpringSecurityToJaasFilter(), JaasApiIntegrationFilter.class);
if (devMode) {
http.headers()
.frameOptions()
.sameOrigin()
.and()
.authorizeRequests()
.antMatchers("/h2-console/**")
.permitAll();
} else {
addLoginPageConfiguration(http);
}
}
protected void addLoginPageConfiguration(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.fullyAuthenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.defaultSuccessUrl("/")
.permitAll()
.and()
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout")
.deleteCookies("JSESSIONID")
.permitAll();
}
protected JaasApiIntegrationFilter jaasApiIntegrationFilter() {
JaasApiIntegrationFilter filter = new JaasApiIntegrationFilter();
filter.setCreateEmptySubject(true);
return filter;
}
private static class CorsWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*");
}
}
}

View File

@ -12,9 +12,6 @@ taskana.schemaName=TASKANA
####### property that control rest api security deploy use true for no security.
devMode=false
####### Property that informs about the Taskana's version. This version is shown the application web
version=@project.version@
####### Properties for AccessIdController to connect to LDAP
taskana.ldap.serverUrl=ldap://localhost:11389
taskana.ldap.bindDn=uid=admin

View File

@ -1,114 +1,21 @@
package pro.taskana;
import java.io.InputStream;
import java.util.Properties;
import javax.annotation.PostConstruct;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
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.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.PlatformTransactionManager;
import pro.taskana.common.rest.RestConfiguration;
import pro.taskana.common.rest.ldap.LdapClient;
import pro.taskana.common.rest.ldap.LdapConfiguration;
import pro.taskana.jobs.TransactionalJobsConfiguration;
import pro.taskana.rest.WebMvcConfig;
import pro.taskana.sampledata.SampleDataGenerator;
/**
* Example Application showing the implementation of taskana-rest-spring for jboss application
* server.
*/
@SpringBootApplication
@EnableScheduling
@SuppressWarnings("checkstyle:Indentation")
@Import({
TransactionalJobsConfiguration.class,
LdapConfiguration.class,
RestConfiguration.class,
WebMvcConfig.class,
})
@SpringBootApplication
@ComponentScan("pro.taskana")
public class TaskanaWildFlyApplication extends SpringBootServletInitializer {
private static final Logger LOGGER = LoggerFactory.getLogger(TaskanaWildFlyApplication.class);
@Value("${taskana.schemaName:TASKANA}")
public String schemaName;
@Value("${generateSampleData:true}")
public boolean generateSampleData;
@Autowired private SampleDataGenerator sampleDataGenerator;
@Autowired private LdapClient ldapClient;
public static void main(String[] args) {
SpringApplication.run(TaskanaWildFlyApplication.class, args);
}
@Bean
@Primary
@ConfigurationProperties(prefix = "datasource")
public DataSourceProperties dataSourceProperties() {
DataSourceProperties props = new DataSourceProperties();
props.setUrl(
"jdbc:h2:mem:taskana;IGNORECASE=TRUE;LOCK_MODE=0;INIT=CREATE SCHEMA IF NOT EXISTS "
+ schemaName);
return props;
}
@Bean
public DataSource dataSource(DataSourceProperties dsProperties) {
// First try to load Properties and get Datasource via jndi lookup
Context ctx;
DataSource dataSource;
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
try (InputStream propertyStream = classloader.getResourceAsStream("application.properties")) {
Properties properties = new Properties();
ctx = new InitialContext();
properties.load(propertyStream);
dataSource = (DataSource) ctx.lookup(properties.getProperty("datasource.jndi"));
return dataSource;
} catch (Exception e) {
LOGGER.error(
"Caught exception {} when attempting to start Taskana with Datasource "
+ "from Jndi. Using default H2 datasource. ",
e);
return dsProperties.initializeDataSourceBuilder().build();
}
}
@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
@DependsOn("getTaskanaEngine") // generate sample data after schema was inserted
public SampleDataGenerator generateSampleData(DataSource dataSource) {
sampleDataGenerator = new SampleDataGenerator(dataSource, schemaName);
return sampleDataGenerator;
}
@PostConstruct
private void init() {
if (generateSampleData) {
sampleDataGenerator.generateSampleData();
}
}
}

View File

@ -0,0 +1,77 @@
package pro.taskana;
import java.io.InputStream;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import pro.taskana.sampledata.SampleDataGenerator;
@Configuration
public class TaskanaWildflyConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(TaskanaWildflyConfiguration.class);
@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
@Primary
@ConfigurationProperties(prefix = "datasource")
public DataSourceProperties dataSourceProperties(
@Value("${taskana.schemaName:TASKANA}") String schemaName) {
DataSourceProperties props = new DataSourceProperties();
props.setUrl(
"jdbc:h2:mem:taskana;IGNORECASE=TRUE;LOCK_MODE=0;INIT=CREATE SCHEMA IF NOT EXISTS "
+ schemaName);
return props;
}
@Bean
@DependsOn("getTaskanaEngine") // generate sample data after schema was inserted
public SampleDataGenerator generateSampleData(
DataSource dataSource,
@Value("${taskana.schemaName:TASKANA}") String schemaName,
@Value("${generateSampleData:true}") boolean generateSampleData) {
SampleDataGenerator sampleDataGenerator = new SampleDataGenerator(dataSource, schemaName);
if (generateSampleData) {
sampleDataGenerator.generateSampleData();
}
return sampleDataGenerator;
}
@Bean
public DataSource dataSource(DataSourceProperties dsProperties) {
// First try to load Properties and get Datasource via jndi lookup
Context ctx;
DataSource dataSource;
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
try (InputStream propertyStream = classloader.getResourceAsStream("application.properties")) {
Properties properties = new Properties();
ctx = new InitialContext();
properties.load(propertyStream);
dataSource = (DataSource) ctx.lookup(properties.getProperty("datasource.jndi"));
return dataSource;
} catch (Exception e) {
LOGGER.error(
"Caught exception when attempting to start Taskana with Datasource "
+ "from Jndi. Using default H2 datasource. ",
e);
return dsProperties.initializeDataSourceBuilder().build();
}
}
}

View File

@ -1,64 +0,0 @@
package pro.taskana.wildfly.security;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Default basic configuration for taskana web example running on Wildfly / JBoss with Elytron or
* JAAS Security.
*/
@Configuration
@EnableWebSecurity
public class WildflyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new CorsWebMvcConfigurer();
}
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilter(jaasApiIntegrationFilter())
.addFilterAfter(new ElytronToJaasFilter(), JaasApiIntegrationFilter.class)
.csrf()
.disable();
}
protected JaasApiIntegrationFilter jaasApiIntegrationFilter() {
JaasApiIntegrationFilter filter = new JaasApiIntegrationFilter();
filter.setCreateEmptySubject(true);
return filter;
}
private static class CorsWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*");
}
}
}

View File

@ -0,0 +1,28 @@
package pro.taskana.wildfly.security;
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.web.jaasapi.JaasApiIntegrationFilter;
/**
* Default basic configuration for taskana web example running on Wildfly / JBoss with Elytron or
* JAAS Security.
*/
@EnableWebSecurity
public class WildflyWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilter(jaasApiIntegrationFilter())
.addFilterAfter(new ElytronToJaasFilter(), JaasApiIntegrationFilter.class)
.csrf()
.disable();
}
protected JaasApiIntegrationFilter jaasApiIntegrationFilter() {
JaasApiIntegrationFilter filter = new JaasApiIntegrationFilter();
filter.setCreateEmptySubject(true);
return filter;
}
}

View File

@ -1,66 +1,28 @@
package pro.taskana;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.Collections;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import pro.taskana.classification.rest.models.ClassificationSummaryRepresentationModel;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventListResource;
import pro.taskana.task.api.models.ObjectReference;
import pro.taskana.common.rest.RestEndpoints;
import pro.taskana.common.test.rest.RestHelper;
import pro.taskana.common.test.rest.TaskanaSpringBootTest;
import pro.taskana.simplehistory.rest.HistoryRestEndpoints;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventPagedRepresentationModel;
import pro.taskana.task.rest.models.ObjectReferenceRepresentationModel;
import pro.taskana.task.rest.models.TaskRepresentationModel;
import pro.taskana.workbasket.rest.models.WorkbasketSummaryRepresentationModel;
@TaskanaSpringBootTest
public class AbstractAccTest {
private static final String AUTHORIZATION_TEAMLEAD_1 = "Basic dGVhbWxlYWQtMTp0ZWFtbGVhZC0x";
/**
* Return a REST template which is capable of dealing with responses in HAL format.
*
* @return RestTemplate
*/
protected static RestTemplate getRestTemplate() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.registerModule(new Jackson2HalModule());
mapper
.registerModule(new ParameterNamesModule())
.registerModule(new Jdk8Module())
.registerModule(new JavaTimeModule());
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Collections.singletonList(MediaTypes.HAL_JSON));
converter.setObjectMapper(mapper);
RestTemplate template = new RestTemplate();
// important to add first to ensure priority
template.getMessageConverters().add(0, converter);
return template;
}
protected HttpHeaders getHeadersTeamlead_1() {
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", AUTHORIZATION_TEAMLEAD_1);
headers.add("Content-Type", "application/json");
return headers;
}
protected RestHelper restHelper = new RestHelper(8080);
protected TaskRepresentationModel getTaskResourceSample() {
ClassificationSummaryRepresentationModel classificationResource =
@ -70,7 +32,7 @@ public class AbstractAccTest {
new WorkbasketSummaryRepresentationModel();
workbasketSummary.setWorkbasketId("WBI:100000000000000000000000000000000004");
ObjectReference objectReference = new ObjectReference();
ObjectReferenceRepresentationModel objectReference = new ObjectReferenceRepresentationModel();
objectReference.setCompany("MyCompany1");
objectReference.setSystem("MySystem1");
objectReference.setSystemInstance("MyInstance1");
@ -84,33 +46,22 @@ public class AbstractAccTest {
return taskRepresentationModel;
}
protected ResponseEntity<TaskHistoryEventListResource> performGetHistoryEventsRestCall() {
HttpEntity<TaskHistoryEventListResource> httpEntity = new HttpEntity<>(getHeadersTeamlead_1());
ResponseEntity<TaskHistoryEventListResource> response =
getRestTemplate()
.exchange(
"http://127.0.0.1:" + "8080" + "/taskana/api/v1/task-history-event",
HttpMethod.GET,
httpEntity,
ParameterizedTypeReference.forType(TaskHistoryEventListResource.class));
return response;
protected ResponseEntity<TaskHistoryEventPagedRepresentationModel>
performGetHistoryEventsRestCall() {
return RestHelper.TEMPLATE.exchange(
restHelper.toUrl("/taskana" + HistoryRestEndpoints.URL_HISTORY_EVENTS),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskHistoryEventPagedRepresentationModel.class));
}
protected ResponseEntity<TaskRepresentationModel> performCreateTaskRestCall() {
TaskRepresentationModel taskRepresentationModel = getTaskResourceSample();
ResponseEntity<TaskRepresentationModel> responseCreateTask =
getRestTemplate()
.exchange(
"http://127.0.0.1:" + "8080" + "/taskana/api/v1/tasks",
HttpMethod.POST,
new HttpEntity<>(taskRepresentationModel, getHeadersTeamlead_1()),
ParameterizedTypeReference.forType(TaskRepresentationModel.class));
return responseCreateTask;
return RestHelper.TEMPLATE.exchange(
restHelper.toUrl("/taskana" + RestEndpoints.URL_TASKS),
HttpMethod.POST,
new HttpEntity<>(taskRepresentationModel, restHelper.getHeadersTeamlead_1()),
ParameterizedTypeReference.forType(TaskRepresentationModel.class));
}
protected String parseServerLog() throws Exception {

View File

@ -1,6 +1,7 @@
package pro.taskana;
import static org.assertj.core.api.Assertions.assertThat;
import static pro.taskana.common.test.rest.RestHelper.TEMPLATE;
import java.io.File;
import java.util.List;
@ -16,11 +17,11 @@ import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import pro.taskana.common.rest.RestEndpoints;
import pro.taskana.common.rest.models.AccessIdRepresentationModel;
import pro.taskana.common.rest.models.TaskanaUserInfoRepresentationModel;
import pro.taskana.task.rest.models.TaskRepresentationModel;
@ -67,14 +68,12 @@ public class TaskanaWildflyTest extends AbstractAccTest {
@Test
@RunAsClient
public void should_ReturnUserInformationForAuthenticatedUser_IfRequested() {
HttpEntity<String> httpEntity = new HttpEntity<>(getHeadersTeamlead_1());
ResponseEntity<TaskanaUserInfoRepresentationModel> response =
getRestTemplate()
.exchange(
"http://127.0.0.1:" + "8080" + "/taskana/api/v1/current-user-info",
HttpMethod.GET,
httpEntity,
ParameterizedTypeReference.forType(TaskanaUserInfoRepresentationModel.class));
TEMPLATE.exchange(
restHelper.toUrl("/taskana" + RestEndpoints.URL_CURRENT_USER),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskanaUserInfoRepresentationModel.class));
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
TaskanaUserInfoRepresentationModel currentUser = response.getBody();
assertThat(currentUser).isNotNull();
@ -86,14 +85,12 @@ public class TaskanaWildflyTest extends AbstractAccTest {
@Test
@RunAsClient
public void should_ReturnUserFromLdap_WhenWildcardSearchIsConducted() {
HttpEntity<String> httpEntity = new HttpEntity<>(getHeadersTeamlead_1());
ResponseEntity<List<AccessIdRepresentationModel>> response =
getRestTemplate()
.exchange(
"http://127.0.0.1:8080/taskana/api/v1/access-ids?search-for=rig",
HttpMethod.GET,
httpEntity,
new ParameterizedTypeReference<List<AccessIdRepresentationModel>>() {});
TEMPLATE.exchange(
restHelper.toUrl("/taskana" + RestEndpoints.URL_ACCESS_ID + "?search-for=rig"),
HttpMethod.GET,
restHelper.defaultRequest(),
new ParameterizedTypeReference<List<AccessIdRepresentationModel>>() {});
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).hasSize(2);
}
@ -101,14 +98,14 @@ public class TaskanaWildflyTest extends AbstractAccTest {
@Test
@RunAsClient
public void should_ReturnTask_WhenRequested() {
HttpEntity<String> httpEntity = new HttpEntity<>(getHeadersTeamlead_1());
ResponseEntity<TaskRepresentationModel> response =
getRestTemplate()
.exchange(
"http://127.0.0.1:8080/taskana/api/v1/tasks/TKI:000000000000000000000000000000000001",
HttpMethod.GET,
httpEntity,
ParameterizedTypeReference.forType(TaskRepresentationModel.class));
TEMPLATE.exchange(
restHelper.toUrl(
"/taskana" + RestEndpoints.URL_TASKS_ID,
"TKI:000000000000000000000000000000000001"),
HttpMethod.GET,
restHelper.defaultRequest(),
ParameterizedTypeReference.forType(TaskRepresentationModel.class));
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
}

View File

@ -24,7 +24,7 @@ import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventListResource;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventPagedRepresentationModel;
import pro.taskana.task.rest.models.TaskRepresentationModel;
/**
@ -95,7 +95,7 @@ public class TaskanaWildflyWithSimpleHistoryAndHistoryLoggerEnabledTest extends
@RunAsClient
public void should_WriteHistoryEventIntoDatabase_And_LogEventToFile() throws Exception {
ResponseEntity<TaskHistoryEventListResource> getHistoryEventsResponse =
ResponseEntity<TaskHistoryEventPagedRepresentationModel> getHistoryEventsResponse =
performGetHistoryEventsRestCall();
assertThat(getHistoryEventsResponse.getBody()).isNotNull();
assertThat(getHistoryEventsResponse.getBody().getLink(IanaLinkRelations.SELF)).isNotNull();

View File

@ -24,7 +24,7 @@ import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventListResource;
import pro.taskana.simplehistory.rest.models.TaskHistoryEventPagedRepresentationModel;
import pro.taskana.task.rest.models.TaskRepresentationModel;
/**
@ -83,7 +83,7 @@ public class TaskanaWildflyWithSimpleHistoryEnabledTest extends AbstractAccTest
@RunAsClient
public void should_WriteHistoryEventIntoDatabase() {
ResponseEntity<TaskHistoryEventListResource> getHistoryEventsResponse =
ResponseEntity<TaskHistoryEventPagedRepresentationModel> getHistoryEventsResponse =
performGetHistoryEventsRestCall();
assertThat(getHistoryEventsResponse.getBody()).isNotNull();
assertThat(getHistoryEventsResponse.getBody().getLink(IanaLinkRelations.SELF)).isNotNull();

View File

@ -81,6 +81,12 @@
<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>
@ -91,12 +97,6 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>pro.taskana</groupId>
<artifactId>taskana-common-data</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
@ -148,10 +148,61 @@
<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.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${version.hibernate}</version>
<scope>test</scope>
</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>compile</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>
@ -163,23 +214,28 @@
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<sourceDirectory>${basedir}/src/test/resources/asciidoc</sourceDirectory>
<doctype>book</doctype>
<attributes>
<snippets>${basedir}/target/generated-snippets</snippets>
<docinfo>shared</docinfo>
</attributes>
<logHandler>
<outputToConsole>false</outputToConsole>
<failIf>
<severity>ERROR</severity>
</failIf>
</logHandler>
</configuration>
</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>

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,91 @@
= TASKANA RESTful API Documentation
== Overview
This is the REST documentation for http://taskana.pro)[TASKANA] - the world's first open source solution for Enterprise Task Management.
*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.
== Task Resource
include::{snippets}/TaskControllerRestDocTest/createTaskDocTest/auto-section.adoc[]
include::{snippets}/TaskControllerRestDocTest/getSpecificTaskDocTest/auto-section.adoc[]
include::{snippets}/TaskControllerRestDocTest/getAllTasksDocTest/auto-section.adoc[]
include::{snippets}/TaskControllerRestDocTest/updateTaskDocTest/auto-section.adoc[]
include::{snippets}/TaskControllerRestDocTest/claimTaskDocTest/auto-section.adoc[]
include::{snippets}/TaskControllerRestDocTest/selectAndClaimTaskDocTest/auto-section.adoc[]
include::{snippets}/TaskControllerRestDocTest/cancelClaimTaskDocTest/auto-section.adoc[]
include::{snippets}/TaskControllerRestDocTest/completeTaskDocTest/auto-section.adoc[]
include::{snippets}/TaskControllerRestDocTest/transferTaskDocTest/auto-section.adoc[]
include::{snippets}/TaskControllerRestDocTest/deleteTaskDocTest/auto-section.adoc[]
include::{snippets}/TaskControllerRestDocTest/deleteTasksDocTest/auto-section.adoc[]
== Task Comment Resource
include::{snippets}/TaskCommentControllerRestDocTest/createTaskCommentDocTest/auto-section.adoc[]
include::{snippets}/TaskCommentControllerRestDocTest/getSpecificTaskCommentDocTest/auto-section.adoc[]
include::{snippets}/TaskCommentControllerRestDocTest/getAllTaskCommentsForSpecificTaskDocTest/auto-section.adoc[]
include::{snippets}/TaskCommentControllerRestDocTest/updateTaskCommentDocTest/auto-section.adoc[]
include::{snippets}/TaskCommentControllerRestDocTest/deleteTaskCommentDocTest/auto-section.adoc[]
== Classification Resource
include::{snippets}/ClassificationControllerRestDocTest/createClassificationDocTest/auto-section.adoc[]
include::{snippets}/ClassificationControllerRestDocTest/getClassificationDocTest/auto-section.adoc[]
include::{snippets}/ClassificationControllerRestDocTest/getAllClassificationsDocTest/auto-section.adoc[]
include::{snippets}/ClassificationControllerRestDocTest/updateClassificationDocTest/auto-section.adoc[]
include::{snippets}/ClassificationControllerRestDocTest/deleteClassificationDocTest/auto-section.adoc[]
== Workbasket Resource
include::{snippets}/WorkbasketControllerRestDocTest/createWorkbasketDocTest/auto-section.adoc[]
include::{snippets}/WorkbasketControllerRestDocTest/getSpecificWorkbasketDocTest/auto-section.adoc[]
include::{snippets}/WorkbasketControllerRestDocTest/getAllWorkbasketsDocTest/auto-section.adoc[]
include::{snippets}/WorkbasketControllerRestDocTest/getAllWorkbasketAccessItemsDocTest/auto-section.adoc[]
include::{snippets}/WorkbasketControllerRestDocTest/getAllWorkbasketDistributionTargetsDocTest/auto-section.adoc[]
include::{snippets}/WorkbasketControllerRestDocTest/updateWorkbasketDocTest/auto-section.adoc[]
include::{snippets}/WorkbasketControllerRestDocTest/removeWorkbasketAsDistributionTargetDocTest/auto-section.adoc[]
include::{snippets}/WorkbasketControllerRestDocTest/setAllWorkbasketAccessItemsDocTest/auto-section.adoc[]
include::{snippets}/WorkbasketControllerRestDocTest/setAllDistributionTargetsDocTest/auto-section.adoc[]
include::{snippets}/WorkbasketControllerRestDocTest/deleteWorkbasketDocTest/auto-section.adoc[]
== Workbasket Access Item Resource
include::{snippets}/WorkbasketAccessItemControllerRestDocTest/getWorkbasketAccessItemsDocTest/auto-section.adoc[]
include::{snippets}/WorkbasketAccessItemControllerRestDocTest/removeWorkbasketAccessItemsDocTest/auto-section.adoc[]
== Monitoring Resources
include::{snippets}/MonitorControllerRestDocTest/getTaskStatusReportDocTest/auto-section.adoc[]
include::{snippets}/MonitorControllerRestDocTest/getWorkbasketReportDocTest/auto-section.adoc[]
include::{snippets}/MonitorControllerRestDocTest/getClassificationReportDocTest/auto-section.adoc[]
include::{snippets}/MonitorControllerRestDocTest/getTimestampReportDocTest/auto-section.adoc[]
== Access Id Resource
include::{snippets}/AccessIdControllerRestDocTest/searchForAccessIdDocTest/auto-section.adoc[]
include::{snippets}/AccessIdControllerRestDocTest/getGroupsForAccessIdDocTest/auto-section.adoc[]
== Configuration Resources
include::{snippets}/TaskanaEngineControllerRestDocTest/getAllDomainsDocTest/auto-section.adoc[]
include::{snippets}/TaskanaEngineControllerRestDocTest/getClassificationCategoriesDocTest/auto-section.adoc[]
include::{snippets}/TaskanaEngineControllerRestDocTest/getClassificationTypesDocTest/auto-section.adoc[]
include::{snippets}/TaskanaEngineControllerRestDocTest/getClassificationCategoriesByTypeMapDocTest/auto-section.adoc[]
include::{snippets}/TaskanaEngineControllerRestDocTest/getCurrentUserInfoDocTest/auto-section.adoc[]
include::{snippets}/TaskanaEngineControllerRestDocTest/getHistoryProviderIsEnabledDocTest/auto-section.adoc[]
include::{snippets}/TaskanaEngineControllerRestDocTest/getCurrentVersionDocTest/auto-section.adoc[]
== Import / Export
include::{snippets}/ClassificationDefinitionControllerRestDocTest/exportClassificationDefinitionsDocTest/auto-section.adoc[]
include::{snippets}/ClassificationDefinitionControllerRestDocTest/importClassificationDefinitionsDocTest/auto-section.adoc[]
include::{snippets}/WorkbasketDefinitionControllerRestDocTest/exportWorkbasketDefinitionsDocTest/auto-section.adoc[]
include::{snippets}/WorkbasketDefinitionControllerRestDocTest/importWorkbasketDefinitionDocTest/auto-section.adoc[]

View File

@ -1,27 +1,25 @@
package pro.taskana.classification.rest;
import java.beans.ConstructorProperties;
import java.util.List;
import java.util.function.BiConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.PagedModel.PageMetadata;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import pro.taskana.classification.api.ClassificationCustomField;
import pro.taskana.classification.api.ClassificationQuery;
import pro.taskana.classification.api.ClassificationService;
import pro.taskana.classification.api.exceptions.ClassificationAlreadyExistException;
@ -32,31 +30,24 @@ import pro.taskana.classification.api.models.ClassificationSummary;
import pro.taskana.classification.rest.assembler.ClassificationRepresentationModelAssembler;
import pro.taskana.classification.rest.assembler.ClassificationSummaryRepresentationModelAssembler;
import pro.taskana.classification.rest.models.ClassificationRepresentationModel;
import pro.taskana.classification.rest.models.ClassificationSummaryRepresentationModel;
import pro.taskana.classification.rest.models.ClassificationSummaryPagedRepresentationModel;
import pro.taskana.common.api.BaseQuery.SortDirection;
import pro.taskana.common.api.exceptions.ConcurrencyException;
import pro.taskana.common.api.exceptions.DomainNotFoundException;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.api.exceptions.NotAuthorizedException;
import pro.taskana.common.rest.AbstractPagingController;
import pro.taskana.common.rest.QueryHelper;
import pro.taskana.common.rest.QueryPagingParameter;
import pro.taskana.common.rest.QuerySortBy;
import pro.taskana.common.rest.QuerySortParameter;
import pro.taskana.common.rest.RestEndpoints;
import pro.taskana.common.rest.models.TaskanaPagedModel;
/** Controller for all {@link Classification} related endpoints. */
@RestController
@EnableHypermediaSupport(type = HypermediaType.HAL)
public class ClassificationController extends AbstractPagingController {
public class ClassificationController {
private static final Logger LOGGER = LoggerFactory.getLogger(ClassificationController.class);
private static final String LIKE = "%";
private static final String NAME = "name";
private static final String NAME_LIKE = "name-like";
private static final String KEY = "key";
private static final String DOMAIN = "domain";
private static final String CATEGORY = "category";
private static final String TYPE = "type";
private final ClassificationService classificationService;
private final ClassificationRepresentationModelAssembler modelAssembler;
private final ClassificationSummaryRepresentationModelAssembler summaryModelAssembler;
@ -71,24 +62,31 @@ public class ClassificationController extends AbstractPagingController {
this.summaryModelAssembler = summaryModelAssembler;
}
/**
* This endpoint retrieves a list of existing Classifications. Filters can be applied.
*
* @title Get a list of all Classifications
* @param filterParameter the filter parameters
* @param sortParameter the sort parameters
* @param pagingParameter the paging parameters
* @return the classifications with the given filter, sort and paging options.
*/
@GetMapping(path = RestEndpoints.URL_CLASSIFICATIONS)
@Transactional(readOnly = true, rollbackFor = Exception.class)
public ResponseEntity<TaskanaPagedModel<ClassificationSummaryRepresentationModel>>
getClassifications(@RequestParam MultiValueMap<String, String> params)
throws InvalidArgumentException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entry to getClassifications(params= {})", params);
}
public ResponseEntity<ClassificationSummaryPagedRepresentationModel> getClassifications(
final ClassificationQueryFilterParameter filterParameter,
final ClassificationQuerySortParameter sortParameter,
final QueryPagingParameter<ClassificationSummary, ClassificationQuery> pagingParameter) {
ClassificationQuery query = classificationService.createClassificationQuery();
applyFilterParams(query, params);
applySortingParams(query, params);
final ClassificationQuery query = classificationService.createClassificationQuery();
filterParameter.applyToQuery(query);
sortParameter.applyToQuery(query);
List<ClassificationSummary> classificationSummaries = pagingParameter.applyToQuery(query);
PageMetadata pageMetadata = getPageMetadata(params, query);
List<ClassificationSummary> classificationSummaries = getQueryList(query, pageMetadata);
ResponseEntity<TaskanaPagedModel<ClassificationSummaryRepresentationModel>> response =
ResponseEntity.ok(summaryModelAssembler.toPageModel(classificationSummaries, pageMetadata));
ResponseEntity<ClassificationSummaryPagedRepresentationModel> response =
ResponseEntity.ok(
summaryModelAssembler.toPagedModel(
classificationSummaries, pagingParameter.getPageMetadata()));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exit from getClassifications(), returning {}", response);
}
@ -96,6 +94,14 @@ public class ClassificationController extends AbstractPagingController {
return response;
}
/**
* This endpoint retrieves a single Classification.
*
* @param classificationId the Id of the requested Classification.
* @return the requested classification
* @throws ClassificationNotFoundException if the requested classification is not found.
* @title Get a single Classification
*/
@GetMapping(path = RestEndpoints.URL_CLASSIFICATIONS_ID, produces = MediaTypes.HAL_JSON_VALUE)
@Transactional(readOnly = true, rollbackFor = Exception.class)
public ResponseEntity<ClassificationRepresentationModel> getClassification(
@ -114,16 +120,29 @@ public class ClassificationController extends AbstractPagingController {
return response;
}
/**
* This endpoint creates a new Classification.
*
* @title Create a new Classification
* @param repModel the Classification which should be created.
* @return The persisted Classification
* @throws NotAuthorizedException if the current user is not allowed to create a Classification.
* @throws ClassificationAlreadyExistException if the new Classification already exists. This
* means that a Classification with the requested key and domain already exist.
* @throws DomainNotFoundException if the domain within the new Classification does not exist.
* @throws InvalidArgumentException if the new Classification does not contain all relevant
* information.
*/
@PostMapping(path = RestEndpoints.URL_CLASSIFICATIONS)
@Transactional(rollbackFor = Exception.class)
public ResponseEntity<ClassificationRepresentationModel> createClassification(
@RequestBody ClassificationRepresentationModel resource)
@RequestBody ClassificationRepresentationModel repModel)
throws NotAuthorizedException, ClassificationAlreadyExistException, DomainNotFoundException,
InvalidArgumentException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entry to createClassification(resource= {})", resource);
LOGGER.debug("Entry to createClassification(repModel= {})", repModel);
}
Classification classification = modelAssembler.toEntityModel(resource);
Classification classification = modelAssembler.toEntityModel(repModel);
classification = classificationService.createClassification(classification);
ResponseEntity<ClassificationRepresentationModel> response =
@ -135,6 +154,19 @@ public class ClassificationController extends AbstractPagingController {
return response;
}
/**
* This endpoint updates a Classification.
*
* @title Update a Classification
* @param classificationId the Id of the Classification which should be updated.
* @param resource the new Classification for the requested id.
* @return the updated Classification
* @throws NotAuthorizedException if the current user is not authorized to update a Classification
* @throws ClassificationNotFoundException if the requested Classification is not found
* @throws ConcurrencyException if the requested Classification Id has been modified in the
* meantime by a different process.
* @throws InvalidArgumentException if the Id in the path and in the request body does not match
*/
@PutMapping(path = RestEndpoints.URL_CLASSIFICATIONS_ID)
@Transactional(rollbackFor = Exception.class)
public ResponseEntity<ClassificationRepresentationModel> updateClassification(
@ -148,20 +180,18 @@ public class ClassificationController extends AbstractPagingController {
classificationId,
resource);
}
ResponseEntity<ClassificationRepresentationModel> result;
if (classificationId.equals(resource.getClassificationId())) {
Classification classification = modelAssembler.toEntityModel(resource);
classification = classificationService.updateClassification(classification);
result = ResponseEntity.ok(modelAssembler.toModel(classification));
} else {
if (!classificationId.equals(resource.getClassificationId())) {
throw new InvalidArgumentException(
"ClassificationId ('"
+ classificationId
+ "') of the URI is not identical with the classificationId ('"
+ resource.getClassificationId()
+ "') of the object in the payload.");
String.format(
"ClassificationId ('%s') of the URI is not identical"
+ " with the classificationId ('%s') of the object in the payload.",
classificationId, resource.getClassificationId()));
}
Classification classification = modelAssembler.toEntityModel(resource);
classification = classificationService.updateClassification(classification);
ResponseEntity<ClassificationRepresentationModel> result =
ResponseEntity.ok(modelAssembler.toModel(classification));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exit from updateClassification(), returning {}", result);
}
@ -169,6 +199,17 @@ public class ClassificationController extends AbstractPagingController {
return result;
}
/**
* This endpoint deletes a requested Classification if possible.
*
* @title Delete a Classification
* @param classificationId the requested Classification Id which should be deleted
* @return no content
* @throws ClassificationNotFoundException if the requested Classification could not be found
* @throws ClassificationInUseException if there are tasks existing referring to the requested
* Classification
* @throws NotAuthorizedException if the user is not authorized to delete a Classification
*/
@DeleteMapping(path = RestEndpoints.URL_CLASSIFICATIONS_ID)
@Transactional(readOnly = true, rollbackFor = Exception.class)
public ResponseEntity<ClassificationRepresentationModel> deleteClassification(
@ -181,88 +222,40 @@ public class ClassificationController extends AbstractPagingController {
return response;
}
private void applySortingParams(ClassificationQuery query, MultiValueMap<String, String> params)
throws InvalidArgumentException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entry to applySortingParams(query= {}, params= {})", query, params);
enum ClassificationQuerySortBy implements QuerySortBy<ClassificationQuery> {
DOMAIN(ClassificationQuery::orderByDomain),
KEY(ClassificationQuery::orderByKey),
CATEGORY(ClassificationQuery::orderByCategory),
NAME(ClassificationQuery::orderByName);
private final BiConsumer<ClassificationQuery, SortDirection> consumer;
ClassificationQuerySortBy(BiConsumer<ClassificationQuery, SortDirection> consumer) {
this.consumer = consumer;
}
QueryHelper.applyAndRemoveSortingParams(
params,
(sortBy, sortDirection) -> {
switch (sortBy) {
case (CATEGORY):
query.orderByCategory(sortDirection);
break;
case (DOMAIN):
query.orderByDomain(sortDirection);
break;
case (KEY):
query.orderByKey(sortDirection);
break;
case (NAME):
query.orderByName(sortDirection);
break;
default:
throw new InvalidArgumentException("Unknown order '" + sortBy + "'");
}
});
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exit from applySortingParams(), returning {}", query);
@Override
public void applySortByForQuery(ClassificationQuery query, SortDirection sortDirection) {
consumer.accept(query, sortDirection);
}
}
private void applyFilterParams(ClassificationQuery query, MultiValueMap<String, String> params)
throws InvalidArgumentException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entry to applyFilterParams(query= {}, params= {})", query, params);
// Unfortunately this class is necessary, since spring can not inject the generic 'sort-by'
// parameter from the super class.
public static class ClassificationQuerySortParameter
extends QuerySortParameter<ClassificationQuery, ClassificationQuerySortBy> {
@ConstructorProperties({"sort-by", "order"})
public ClassificationQuerySortParameter(
List<ClassificationQuerySortBy> sortBy, List<SortDirection> order)
throws InvalidArgumentException {
super(sortBy, order);
}
if (params.containsKey(NAME)) {
String[] names = extractCommaSeparatedFields(params.get(NAME));
query.nameIn(names);
params.remove(NAME);
}
if (params.containsKey(NAME_LIKE)) {
query.nameLike(LIKE + params.get(NAME_LIKE).get(0) + LIKE);
params.remove(NAME_LIKE);
}
if (params.containsKey(KEY)) {
String[] names = extractCommaSeparatedFields(params.get(KEY));
query.keyIn(names);
params.remove(KEY);
}
if (params.containsKey(CATEGORY)) {
String[] names = extractCommaSeparatedFields(params.get(CATEGORY));
query.categoryIn(names);
params.remove(CATEGORY);
}
if (params.containsKey(DOMAIN)) {
String[] names = extractCommaSeparatedFields(params.get(DOMAIN));
query.domainIn(names);
params.remove(DOMAIN);
}
if (params.containsKey(TYPE)) {
String[] names = extractCommaSeparatedFields(params.get(TYPE));
query.typeIn(names);
params.remove(TYPE);
}
for (ClassificationCustomField customField : ClassificationCustomField.values()) {
List<String> customFieldParams =
params.remove(customField.name().replace("_", "-").toLowerCase() + "-like");
if (customFieldParams != null) {
String[] customValues = extractCommaSeparatedFields(customFieldParams);
for (int i = 0; i < customValues.length; i++) {
customValues[i] = LIKE + customValues[i] + LIKE;
}
query.customAttributeLike(customField, customValues);
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exit from applyFilterParams(), returning {}", query);
// this getter is necessary for the documentation!
@Override
public List<ClassificationQuerySortBy> getSortBy() {
return super.getSortBy();
}
}
}

View File

@ -2,10 +2,10 @@ package pro.taskana.classification.rest;
import static pro.taskana.common.internal.util.CheckedFunction.wrap;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
@ -33,17 +33,17 @@ import pro.taskana.classification.api.exceptions.ClassificationAlreadyExistExcep
import pro.taskana.classification.api.exceptions.ClassificationNotFoundException;
import pro.taskana.classification.api.models.Classification;
import pro.taskana.classification.api.models.ClassificationSummary;
import pro.taskana.classification.rest.assembler.ClassificationRepresentationModelAssembler;
import pro.taskana.classification.rest.assembler.ClassificationDefinitionCollectionRepresentationModel;
import pro.taskana.classification.rest.assembler.ClassificationDefinitionRepresentationModelAssembler;
import pro.taskana.classification.rest.models.ClassificationDefinitionRepresentationModel;
import pro.taskana.classification.rest.models.ClassificationRepresentationModel;
import pro.taskana.common.api.exceptions.ConcurrencyException;
import pro.taskana.common.api.exceptions.DomainNotFoundException;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.api.exceptions.NotAuthorizedException;
import pro.taskana.common.rest.RestEndpoints;
import pro.taskana.common.rest.models.TaskanaPagedModel;
/** Controller for Importing / Exporting classifications. */
@SuppressWarnings("unused")
@RestController
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class ClassificationDefinitionController {
@ -53,39 +53,47 @@ public class ClassificationDefinitionController {
private final ObjectMapper mapper;
private final ClassificationService classificationService;
private final ClassificationRepresentationModelAssembler
classificationRepresentationModelAssembler;
private final ClassificationDefinitionRepresentationModelAssembler assembler;
@Autowired
ClassificationDefinitionController(
ObjectMapper mapper,
ClassificationService classificationService,
ClassificationRepresentationModelAssembler classificationRepresentationModelAssembler) {
ClassificationDefinitionRepresentationModelAssembler assembler) {
this.mapper = mapper;
this.classificationService = classificationService;
this.classificationRepresentationModelAssembler = classificationRepresentationModelAssembler;
this.assembler = assembler;
}
/**
* This endpoint exports all configured Classifications.
*
* @param domain Filter the export by domain
* @return the configured Classifications.
* @title Export Classifications
*/
@GetMapping(path = RestEndpoints.URL_CLASSIFICATION_DEFINITIONS)
@Transactional(readOnly = true, rollbackFor = Exception.class)
public ResponseEntity<TaskanaPagedModel<ClassificationRepresentationModel>> exportClassifications(
@RequestParam(required = false) String domain) {
LOGGER.debug("Entry to exportClassifications(domain= {})", domain);
public ResponseEntity<ClassificationDefinitionCollectionRepresentationModel>
exportClassifications(@RequestParam(required = false) String[] domain) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entry to exportClassifications(domain= {})", Arrays.toString(domain));
}
ClassificationQuery query = classificationService.createClassificationQuery();
List<ClassificationSummary> summaries =
domain != null ? query.domainIn(domain).list() : query.list();
TaskanaPagedModel<ClassificationRepresentationModel> pageModel =
ClassificationDefinitionCollectionRepresentationModel collectionModel =
summaries.stream()
.map(ClassificationSummary::getId)
.map(wrap(classificationService::getClassification))
.collect(
Collectors.collectingAndThen(
Collectors.toList(), classificationRepresentationModelAssembler::toPageModel));
Collectors.toList(), assembler::toTaskanaCollectionModel));
ResponseEntity<TaskanaPagedModel<ClassificationRepresentationModel>> response =
ResponseEntity.ok(pageModel);
ResponseEntity<ClassificationDefinitionCollectionRepresentationModel> response =
ResponseEntity.ok(collectionModel);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exit from exportClassifications(), returning {}", response);
}
@ -93,6 +101,21 @@ public class ClassificationDefinitionController {
return response;
}
/**
* This endpoint imports all Classifications. Existing Classifications will not be removed.
* Existing Classifications with the same key/domain will be overridden.
*
* @title Import Classifications
* @param file the file containing the Classifications which should be imported.
* @return nothing
* @throws InvalidArgumentException if any Classification within the import file is invalid
* @throws NotAuthorizedException if the current user is not authorized to import Classifications
* @throws ConcurrencyException TODO: this makes no sense
* @throws ClassificationNotFoundException TODO: this makes no sense
* @throws ClassificationAlreadyExistException TODO: this makes no sense
* @throws DomainNotFoundException if the domain for a specific Classification does not exist
* @throws IOException if the import file could not be parsed
*/
@PostMapping(path = RestEndpoints.URL_CLASSIFICATION_DEFINITIONS)
@Transactional(rollbackFor = Exception.class)
public ResponseEntity<Void> importClassifications(@RequestParam("file") MultipartFile file)
@ -101,13 +124,13 @@ public class ClassificationDefinitionController {
DomainNotFoundException, IOException {
LOGGER.debug("Entry to importClassifications()");
Map<String, String> systemIds = getSystemIds();
TaskanaPagedModel<ClassificationRepresentationModel> classificationsResources =
ClassificationDefinitionCollectionRepresentationModel collection =
extractClassificationResourcesFromFile(file);
checkForDuplicates(classificationsResources.getContent());
checkForDuplicates(collection.getContent());
Map<Classification, String> childrenInFile =
mapChildrenToParentKeys(classificationsResources.getContent(), systemIds);
insertOrUpdateClassificationsWithoutParent(classificationsResources.getContent(), systemIds);
mapChildrenToParentKeys(collection.getContent(), systemIds);
insertOrUpdateClassificationsWithoutParent(collection.getContent(), systemIds);
updateParentChildrenRelations(childrenInFile);
ResponseEntity<Void> response = ResponseEntity.noContent().build();
LOGGER.debug("Exit from importClassifications(), returning {}", response);
@ -120,18 +143,18 @@ public class ClassificationDefinitionController {
Collectors.toMap(i -> i.getKey() + "|" + i.getDomain(), ClassificationSummary::getId));
}
private TaskanaPagedModel<ClassificationRepresentationModel>
private ClassificationDefinitionCollectionRepresentationModel
extractClassificationResourcesFromFile(MultipartFile file) throws IOException {
return mapper.readValue(
file.getInputStream(),
new TypeReference<TaskanaPagedModel<ClassificationRepresentationModel>>() {});
file.getInputStream(), ClassificationDefinitionCollectionRepresentationModel.class);
}
private void checkForDuplicates(
Collection<ClassificationRepresentationModel> classificationList) {
Collection<ClassificationDefinitionRepresentationModel> definitionList) {
List<String> identifiers = new ArrayList<>();
Set<String> duplicates = new HashSet<>();
for (ClassificationRepresentationModel classification : classificationList) {
for (ClassificationDefinitionRepresentationModel definition : definitionList) {
ClassificationRepresentationModel classification = definition.getClassification();
String identifier = classification.getKey() + "|" + classification.getDomain();
if (identifiers.contains(identifier)) {
duplicates.add(identifier);
@ -146,20 +169,23 @@ public class ClassificationDefinitionController {
}
private Map<Classification, String> mapChildrenToParentKeys(
Collection<ClassificationRepresentationModel> classificationRepresentationModels,
Collection<ClassificationDefinitionRepresentationModel> definitionList,
Map<String, String> systemIds) {
LOGGER.debug("Entry to mapChildrenToParentKeys()");
Map<Classification, String> childrenInFile = new HashMap<>();
Set<String> newKeysWithDomain = new HashSet<>();
classificationRepresentationModels.forEach(
cl -> newKeysWithDomain.add(cl.getKey() + "|" + cl.getDomain()));
definitionList.stream()
.map(ClassificationDefinitionRepresentationModel::getClassification)
.forEach(cl -> newKeysWithDomain.add(cl.getKey() + "|" + cl.getDomain()));
for (ClassificationRepresentationModel cl : classificationRepresentationModels) {
for (ClassificationDefinitionRepresentationModel def : definitionList) {
ClassificationRepresentationModel cl = def.getClassification();
cl.setParentId(cl.getParentId() == null ? "" : cl.getParentId());
cl.setParentKey(cl.getParentKey() == null ? "" : cl.getParentKey());
if (!cl.getParentId().equals("") && cl.getParentKey().equals("")) {
for (ClassificationRepresentationModel parent : classificationRepresentationModels) {
for (ClassificationDefinitionRepresentationModel parentDef : definitionList) {
ClassificationRepresentationModel parent = parentDef.getClassification();
if (cl.getParentId().equals(parent.getClassificationId())) {
cl.setParentKey(parent.getKey());
}
@ -171,8 +197,7 @@ public class ClassificationDefinitionController {
&& !cl.getParentKey().equals("")
&& (newKeysWithDomain.contains(parentKeyAndDomain)
|| systemIds.containsKey(parentKeyAndDomain)))) {
childrenInFile.put(
classificationRepresentationModelAssembler.toEntityModel(cl), cl.getParentKey());
childrenInFile.put(assembler.toEntityModel(def), cl.getParentKey());
}
}
if (LOGGER.isDebugEnabled()) {
@ -183,29 +208,26 @@ public class ClassificationDefinitionController {
}
private void insertOrUpdateClassificationsWithoutParent(
Collection<ClassificationRepresentationModel> classificationRepresentationModels,
Collection<ClassificationDefinitionRepresentationModel> definitionList,
Map<String, String> systemIds)
throws ClassificationNotFoundException, NotAuthorizedException, InvalidArgumentException,
ClassificationAlreadyExistException, DomainNotFoundException, ConcurrencyException {
LOGGER.debug("Entry to insertOrUpdateClassificationsWithoutParent()");
for (ClassificationRepresentationModel classificationRepresentationModel :
classificationRepresentationModels) {
classificationRepresentationModel.setParentKey(null);
classificationRepresentationModel.setParentId(null);
classificationRepresentationModel.setClassificationId(null);
for (ClassificationDefinitionRepresentationModel definition : definitionList) {
ClassificationRepresentationModel classificationRepModel = definition.getClassification();
classificationRepModel.setParentKey(null);
classificationRepModel.setParentId(null);
classificationRepModel.setClassificationId(null);
Classification newClassification = assembler.toEntityModel(definition);
String systemId =
systemIds.get(
classificationRepresentationModel.getKey()
+ "|"
+ classificationRepresentationModel.getDomain());
systemIds.get(classificationRepModel.getKey() + "|" + classificationRepModel.getDomain());
if (systemId != null) {
updateExistingClassification(classificationRepresentationModel, systemId);
updateExistingClassification(newClassification, systemId);
} else {
classificationService.createClassification(
classificationRepresentationModelAssembler.toEntityModel(
classificationRepresentationModel));
classificationService.createClassification(newClassification);
}
}
LOGGER.debug("Exit from insertOrUpdateClassificationsWithoutParent()");
@ -237,31 +259,48 @@ public class ClassificationDefinitionController {
LOGGER.debug("Exit from updateParentChildrenRelations()");
}
private void updateExistingClassification(ClassificationRepresentationModel cl, String systemId)
private void updateExistingClassification(Classification newClassification, String systemId)
throws ClassificationNotFoundException, NotAuthorizedException, ConcurrencyException,
InvalidArgumentException {
LOGGER.debug("Entry to updateExistingClassification()");
Classification currentClassification = classificationService.getClassification(systemId);
if (cl.getType() != null && !cl.getType().equals(currentClassification.getType())) {
if (newClassification.getType() != null
&& !newClassification.getType().equals(currentClassification.getType())) {
throw new InvalidArgumentException("Can not change the type of a classification.");
}
currentClassification.setCategory(cl.getCategory());
currentClassification.setIsValidInDomain(cl.getIsValidInDomain());
currentClassification.setName(cl.getName());
currentClassification.setParentId(cl.getParentId());
currentClassification.setParentKey(cl.getParentKey());
currentClassification.setDescription(cl.getDescription());
currentClassification.setPriority(cl.getPriority());
currentClassification.setServiceLevel(cl.getServiceLevel());
currentClassification.setApplicationEntryPoint(cl.getApplicationEntryPoint());
currentClassification.setCustomAttribute(ClassificationCustomField.CUSTOM_1, cl.getCustom1());
currentClassification.setCustomAttribute(ClassificationCustomField.CUSTOM_2, cl.getCustom2());
currentClassification.setCustomAttribute(ClassificationCustomField.CUSTOM_3, cl.getCustom3());
currentClassification.setCustomAttribute(ClassificationCustomField.CUSTOM_4, cl.getCustom4());
currentClassification.setCustomAttribute(ClassificationCustomField.CUSTOM_5, cl.getCustom5());
currentClassification.setCustomAttribute(ClassificationCustomField.CUSTOM_6, cl.getCustom6());
currentClassification.setCustomAttribute(ClassificationCustomField.CUSTOM_7, cl.getCustom7());
currentClassification.setCustomAttribute(ClassificationCustomField.CUSTOM_8, cl.getCustom8());
currentClassification.setCategory(newClassification.getCategory());
currentClassification.setIsValidInDomain(newClassification.getIsValidInDomain());
currentClassification.setName(newClassification.getName());
currentClassification.setParentId(newClassification.getParentId());
currentClassification.setParentKey(newClassification.getParentKey());
currentClassification.setDescription(newClassification.getDescription());
currentClassification.setPriority(newClassification.getPriority());
currentClassification.setServiceLevel(newClassification.getServiceLevel());
currentClassification.setApplicationEntryPoint(newClassification.getApplicationEntryPoint());
currentClassification.setCustomAttribute(
ClassificationCustomField.CUSTOM_1,
newClassification.getCustomAttribute(ClassificationCustomField.CUSTOM_1));
currentClassification.setCustomAttribute(
ClassificationCustomField.CUSTOM_2,
newClassification.getCustomAttribute(ClassificationCustomField.CUSTOM_2));
currentClassification.setCustomAttribute(
ClassificationCustomField.CUSTOM_3,
newClassification.getCustomAttribute(ClassificationCustomField.CUSTOM_3));
currentClassification.setCustomAttribute(
ClassificationCustomField.CUSTOM_4,
newClassification.getCustomAttribute(ClassificationCustomField.CUSTOM_4));
currentClassification.setCustomAttribute(
ClassificationCustomField.CUSTOM_5,
newClassification.getCustomAttribute(ClassificationCustomField.CUSTOM_5));
currentClassification.setCustomAttribute(
ClassificationCustomField.CUSTOM_6,
newClassification.getCustomAttribute(ClassificationCustomField.CUSTOM_6));
currentClassification.setCustomAttribute(
ClassificationCustomField.CUSTOM_7,
newClassification.getCustomAttribute(ClassificationCustomField.CUSTOM_7));
currentClassification.setCustomAttribute(
ClassificationCustomField.CUSTOM_8,
newClassification.getCustomAttribute(ClassificationCustomField.CUSTOM_8));
classificationService.updateClassification(currentClassification);
LOGGER.debug("Exit from updateExistingClassification()");
}

View File

@ -0,0 +1,176 @@
package pro.taskana.classification.rest;
import static pro.taskana.common.internal.util.CheckedConsumer.wrap;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.beans.ConstructorProperties;
import java.util.Optional;
import java.util.stream.Stream;
import pro.taskana.classification.api.ClassificationCustomField;
import pro.taskana.classification.api.ClassificationQuery;
import pro.taskana.common.internal.util.Pair;
import pro.taskana.common.rest.QueryParameter;
public class ClassificationQueryFilterParameter
implements QueryParameter<ClassificationQuery, Void> {
/** Filter by the name of the classification. This is an exact match. */
private final String[] name;
/**
* Filter by the name of the classification. This results in a substring search. (% is appended to
* the beginning and end of the requested value). Further SQL "LIKE" wildcard characters will be
* resolved correctly.
*/
@JsonProperty("name-like")
private final String[] nameLike;
/** Filter by the key of the classification. This is an exact match. */
private final String[] key;
/** Filter by the category of the classification. This is an exact match. */
private final String[] category;
/** Filter by the domain of the classification. This is an exact match. */
private final String[] domain;
/** Filter by the type of the classification. This is an exact match. */
private final String[] type;
/**
* Filter by the value of the field custom1. This results in a substring search.. (% is appended
* to the beginning and end of the requested value). Further SQL "LIKE" wildcard characters will
* be resolved correctly.
*/
@JsonProperty("custom-1-like")
private final String[] custom1Like;
/**
* Filter by the value of the field custom2. This results in a substring search.. (% is appended
* to the beginning and end of the requested value). Further SQL "LIKE" wildcard characters will
* be resolved correctly.
*/
@JsonProperty("custom-2-like")
private final String[] custom2Like;
/**
* Filter by the value of the field custom3. This results in a substring search.. (% is appended
* to the beginning and end of the requested value). Further SQL "LIKE" wildcard characters will
* be resolved correctly.
*/
@JsonProperty("custom-3-like")
private final String[] custom3Like;
/**
* Filter by the value of the field custom4. This results in a substring search.. (% is appended
* to the beginning and end of the requested value). Further SQL "LIKE" wildcard characters will
* be resolved correctly.
*/
@JsonProperty("custom-4-like")
private final String[] custom4Like;
/**
* Filter by the value of the field custom5. This results in a substring search.. (% is appended
* to the beginning and end of the requested value). Further SQL "LIKE" wildcard characters will
* be resolved correctly.
*/
@JsonProperty("custom-5-like")
private final String[] custom5Like;
/**
* Filter by the value of the field custom6. This results in a substring search.. (% is appended
* to the beginning and end of the requested value). Further SQL "LIKE" wildcard characters will
* be resolved correctly.
*/
@JsonProperty("custom-6-like")
private final String[] custom6Like;
/**
* Filter by the value of the field custom7. This results in a substring search.. (% is appended
* to the beginning and end of the requested value). Further SQL "LIKE" wildcard characters will
* be resolved correctly.
*/
@JsonProperty("custom-7-like")
private final String[] custom7Like;
/**
* Filter by the value of the field custom8. This results in a substring search.. (% is appended
* to the beginning and end of the requested value). Further SQL "LIKE" wildcard characters will
* be resolved correctly.
*/
@JsonProperty("custom-8-like")
private final String[] custom8Like;
@SuppressWarnings("indentation")
@ConstructorProperties({
"name",
"name-like",
"key",
"category",
"domain",
"type",
"custom-1-like",
"custom-2-like",
"custom-3-like",
"custom-4-like",
"custom-5-like",
"custom-6-like",
"custom-7-like",
"custom-8-like"
})
public ClassificationQueryFilterParameter(
String[] name,
String[] nameLike,
String[] key,
String[] category,
String[] domain,
String[] type,
String[] custom1Like,
String[] custom2Like,
String[] custom3Like,
String[] custom4Like,
String[] custom5Like,
String[] custom6Like,
String[] custom7Like,
String[] custom8Like) {
this.name = name;
this.nameLike = nameLike;
this.key = key;
this.category = category;
this.domain = domain;
this.type = type;
this.custom1Like = custom1Like;
this.custom2Like = custom2Like;
this.custom3Like = custom3Like;
this.custom4Like = custom4Like;
this.custom5Like = custom5Like;
this.custom6Like = custom6Like;
this.custom7Like = custom7Like;
this.custom8Like = custom8Like;
}
@Override
public Void applyToQuery(ClassificationQuery query) {
Optional.ofNullable(name).ifPresent(query::nameIn);
Optional.ofNullable(nameLike).map(this::wrapElementsInLikeStatement).ifPresent(query::nameLike);
Optional.ofNullable(key).ifPresent(query::keyIn);
Optional.ofNullable(category).ifPresent(query::categoryIn);
Optional.ofNullable(domain).ifPresent(query::domainIn);
Optional.ofNullable(type).ifPresent(query::typeIn);
Stream.of(
Pair.of(ClassificationCustomField.CUSTOM_1, custom1Like),
Pair.of(ClassificationCustomField.CUSTOM_2, custom2Like),
Pair.of(ClassificationCustomField.CUSTOM_3, custom3Like),
Pair.of(ClassificationCustomField.CUSTOM_4, custom4Like),
Pair.of(ClassificationCustomField.CUSTOM_5, custom5Like),
Pair.of(ClassificationCustomField.CUSTOM_6, custom6Like),
Pair.of(ClassificationCustomField.CUSTOM_7, custom7Like),
Pair.of(ClassificationCustomField.CUSTOM_8, custom8Like))
.forEach(
pair ->
Optional.ofNullable(pair.getRight())
.map(this::wrapElementsInLikeStatement)
.ifPresent(wrap(l -> query.customAttributeLike(pair.getLeft(), l))));
return null;
}
}

View File

@ -0,0 +1,27 @@
package pro.taskana.classification.rest.assembler;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.beans.ConstructorProperties;
import java.util.Collection;
import pro.taskana.classification.rest.models.ClassificationDefinitionRepresentationModel;
import pro.taskana.common.rest.models.CollectionRepresentationModel;
public class ClassificationDefinitionCollectionRepresentationModel extends
CollectionRepresentationModel<ClassificationDefinitionRepresentationModel> {
@ConstructorProperties("classifications")
public ClassificationDefinitionCollectionRepresentationModel(
Collection<ClassificationDefinitionRepresentationModel> content) {
super(content);
}
/**
* the embedded classification definitions.
*/
@JsonProperty("classifications")
@Override
public Collection<ClassificationDefinitionRepresentationModel> getContent() {
return super.getContent();
}
}

View File

@ -0,0 +1,49 @@
package pro.taskana.classification.rest.assembler;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.server.RepresentationModelAssembler;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import pro.taskana.classification.api.models.Classification;
import pro.taskana.classification.rest.models.ClassificationDefinitionRepresentationModel;
import pro.taskana.classification.rest.models.ClassificationRepresentationModel;
import pro.taskana.common.rest.assembler.CollectionRepresentationModelAssembler;
@Component
public class ClassificationDefinitionRepresentationModelAssembler
implements RepresentationModelAssembler<
Classification, ClassificationDefinitionRepresentationModel>,
CollectionRepresentationModelAssembler<
Classification,
ClassificationDefinitionRepresentationModel,
ClassificationDefinitionCollectionRepresentationModel> {
private final ClassificationRepresentationModelAssembler classificationAssembler;
@Autowired
public ClassificationDefinitionRepresentationModelAssembler(
ClassificationRepresentationModelAssembler classificationAssembler) {
this.classificationAssembler = classificationAssembler;
}
@Override
@NonNull
public ClassificationDefinitionRepresentationModel toModel(
@NonNull Classification classification) {
ClassificationRepresentationModel classificationRepModel =
classificationAssembler.toModel(classification);
return new ClassificationDefinitionRepresentationModel(classificationRepModel);
}
@Override
public ClassificationDefinitionCollectionRepresentationModel buildCollectionEntity(
List<ClassificationDefinitionRepresentationModel> content) {
return new ClassificationDefinitionCollectionRepresentationModel(content);
}
public Classification toEntityModel(ClassificationDefinitionRepresentationModel repModel) {
return classificationAssembler.toEntityModel(repModel.getClassification());
}
}

View File

@ -2,6 +2,7 @@ package pro.taskana.classification.rest.assembler;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.lang.NonNull;
@ -13,10 +14,10 @@ import pro.taskana.classification.api.exceptions.ClassificationNotFoundException
import pro.taskana.classification.api.models.Classification;
import pro.taskana.classification.internal.models.ClassificationImpl;
import pro.taskana.classification.rest.ClassificationController;
import pro.taskana.classification.rest.models.ClassificationCollectionRepresentationModel;
import pro.taskana.classification.rest.models.ClassificationRepresentationModel;
import pro.taskana.common.api.exceptions.SystemException;
import pro.taskana.common.rest.assembler.TaskanaPagingAssembler;
import pro.taskana.common.rest.models.TaskanaPagedModelKeys;
import pro.taskana.common.rest.assembler.CollectionRepresentationModelAssembler;
/**
* Transforms {@link Classification} to its resource counterpart {@link
@ -24,7 +25,10 @@ import pro.taskana.common.rest.models.TaskanaPagedModelKeys;
*/
@Component
public class ClassificationRepresentationModelAssembler
implements TaskanaPagingAssembler<Classification, ClassificationRepresentationModel> {
implements CollectionRepresentationModelAssembler<
Classification,
ClassificationRepresentationModel,
ClassificationCollectionRepresentationModel> {
final ClassificationService classificationService;
@ -36,6 +40,20 @@ public class ClassificationRepresentationModelAssembler
@NonNull
@Override
public ClassificationRepresentationModel toModel(@NonNull Classification classification) {
ClassificationRepresentationModel repModel = toModelWithoutLinks(classification);
try {
repModel.add(
WebMvcLinkBuilder.linkTo(
methodOn(ClassificationController.class)
.getClassification(classification.getId()))
.withSelfRel());
} catch (ClassificationNotFoundException e) {
throw new SystemException("caught unexpected Exception.", e.getCause());
}
return repModel;
}
public ClassificationRepresentationModel toModelWithoutLinks(Classification classification) {
ClassificationRepresentationModel repModel = new ClassificationRepresentationModel();
repModel.setClassificationId(classification.getId());
repModel.setApplicationEntryPoint(classification.getApplicationEntryPoint());
@ -60,23 +78,9 @@ public class ClassificationRepresentationModelAssembler
repModel.setCreated(classification.getCreated());
repModel.setModified(classification.getModified());
repModel.setDescription(classification.getDescription());
try {
repModel.add(
WebMvcLinkBuilder.linkTo(
methodOn(ClassificationController.class)
.getClassification(classification.getId()))
.withSelfRel());
} catch (ClassificationNotFoundException e) {
throw new SystemException("caught unexpected Exception.", e.getCause());
}
return repModel;
}
@Override
public TaskanaPagedModelKeys getProperty() {
return TaskanaPagedModelKeys.CLASSIFICATIONS;
}
public Classification toEntityModel(ClassificationRepresentationModel repModel) {
ClassificationImpl classification =
(ClassificationImpl)
@ -105,4 +109,10 @@ public class ClassificationRepresentationModelAssembler
classification.setModified(repModel.getModified());
return classification;
}
@Override
public ClassificationCollectionRepresentationModel buildCollectionEntity(
List<ClassificationRepresentationModel> content) {
return new ClassificationCollectionRepresentationModel(content);
}
}

View File

@ -8,26 +8,27 @@ import static pro.taskana.classification.api.ClassificationCustomField.CUSTOM_5;
import static pro.taskana.classification.api.ClassificationCustomField.CUSTOM_6;
import static pro.taskana.classification.api.ClassificationCustomField.CUSTOM_7;
import static pro.taskana.classification.api.ClassificationCustomField.CUSTOM_8;
import static pro.taskana.common.rest.models.TaskanaPagedModelKeys.CLASSIFICATIONS;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.PagedModel.PageMetadata;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import pro.taskana.classification.api.ClassificationService;
import pro.taskana.classification.api.models.ClassificationSummary;
import pro.taskana.classification.internal.models.ClassificationSummaryImpl;
import pro.taskana.classification.rest.models.ClassificationSummaryPagedRepresentationModel;
import pro.taskana.classification.rest.models.ClassificationSummaryRepresentationModel;
import pro.taskana.common.rest.assembler.TaskanaPagingAssembler;
import pro.taskana.common.rest.models.TaskanaPagedModel;
import pro.taskana.common.rest.models.TaskanaPagedModelKeys;
import pro.taskana.common.rest.assembler.PagedRepresentationModelAssembler;
import pro.taskana.common.rest.models.PageMetadata;
/** EntityModel assembler for {@link ClassificationSummaryRepresentationModel}. */
@Component
public class ClassificationSummaryRepresentationModelAssembler
implements TaskanaPagingAssembler<
ClassificationSummary, ClassificationSummaryRepresentationModel> {
implements PagedRepresentationModelAssembler<
ClassificationSummary,
ClassificationSummaryRepresentationModel,
ClassificationSummaryPagedRepresentationModel> {
private final ClassificationService classificationService;
@ -91,14 +92,8 @@ public class ClassificationSummaryRepresentationModelAssembler
}
@Override
public TaskanaPagedModelKeys getProperty() {
return CLASSIFICATIONS;
}
@Override
public TaskanaPagedModel<ClassificationSummaryRepresentationModel> toPageModel(
Iterable<ClassificationSummary> entities, PageMetadata pageMetadata) {
return addLinksToPagedResource(
TaskanaPagingAssembler.super.toPageModel(entities, pageMetadata));
public ClassificationSummaryPagedRepresentationModel buildPageableEntity(
Collection<ClassificationSummaryRepresentationModel> content, PageMetadata pageMetadata) {
return new ClassificationSummaryPagedRepresentationModel(content, pageMetadata);
}
}

View File

@ -0,0 +1,24 @@
package pro.taskana.classification.rest.models;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.beans.ConstructorProperties;
import java.util.Collection;
import pro.taskana.common.rest.models.CollectionRepresentationModel;
public class ClassificationCollectionRepresentationModel
extends CollectionRepresentationModel<ClassificationRepresentationModel> {
@ConstructorProperties("classifications")
public ClassificationCollectionRepresentationModel(
Collection<ClassificationRepresentationModel> content) {
super(content);
}
/** the embedded classifications. */
@Override
@JsonProperty("classifications")
public Collection<ClassificationRepresentationModel> getContent() {
return super.getContent();
}
}

View File

@ -0,0 +1,25 @@
package pro.taskana.classification.rest.models;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import org.springframework.hateoas.RepresentationModel;
public class ClassificationDefinitionRepresentationModel extends
RepresentationModel<ClassificationDefinitionRepresentationModel> {
@JsonIgnoreProperties("_links")
@JsonUnwrapped
private final ClassificationRepresentationModel classification;
@JsonCreator
public ClassificationDefinitionRepresentationModel(
ClassificationRepresentationModel classification) {
this.classification = classification;
}
public ClassificationRepresentationModel getClassification() {
return classification;
}
}

View File

@ -7,9 +7,21 @@ import pro.taskana.classification.api.models.Classification;
/** EntityModel class for {@link Classification}. */
public class ClassificationRepresentationModel extends ClassificationSummaryRepresentationModel {
/** True, if this classification to objects in this domain. */
private Boolean isValidInDomain;
private Instant created; // ISO-8601
private Instant modified; // ISO-8601
/**
* The creation timestamp of the classification in the system.
*
* <p>The format is ISO-8601.
*/
private Instant created;
/**
* The timestamp of the last modification.
*
* <p>The format is ISO-8601.
*/
private Instant modified;
/** The description of the classification. */
private String description;
public Boolean getIsValidInDomain() {

View File

@ -0,0 +1,26 @@
package pro.taskana.classification.rest.models;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.beans.ConstructorProperties;
import java.util.Collection;
import javax.validation.constraints.NotNull;
import pro.taskana.common.rest.models.PageMetadata;
import pro.taskana.common.rest.models.PagedRepresentationModel;
public class ClassificationSummaryPagedRepresentationModel
extends PagedRepresentationModel<ClassificationSummaryRepresentationModel> {
@ConstructorProperties({"classifications", "page"})
public ClassificationSummaryPagedRepresentationModel(
Collection<ClassificationSummaryRepresentationModel> content, PageMetadata pageMetadata) {
super(content, pageMetadata);
}
/** the embedded classifications. */
@Override
@JsonProperty("classifications")
public @NotNull Collection<ClassificationSummaryRepresentationModel> getContent() {
return super.getContent();
}
}

View File

@ -1,5 +1,6 @@
package pro.taskana.classification.rest.models;
import javax.validation.constraints.NotNull;
import org.springframework.hateoas.RepresentationModel;
import pro.taskana.classification.api.models.ClassificationSummary;
@ -8,24 +9,56 @@ import pro.taskana.classification.api.models.ClassificationSummary;
public class ClassificationSummaryRepresentationModel
extends RepresentationModel<ClassificationSummaryRepresentationModel> {
protected String classificationId;
protected String key;
/** Unique Id. */
@NotNull protected String classificationId;
/**
* The key of the Classification. This is typically an externally known code or abbreviation of
* the Classification.
*/
@NotNull protected String key;
/**
* The logical name of the entry point. This is needed by the task list application to determine
* the redirect to work on a task of this Classification.
*/
protected String applicationEntryPoint;
protected String category;
/**
* The category of the classification. Categories can be configured in the file
* 'taskana.properties'.
*/
@NotNull protected String category;
/** The domain for which this classification is specified. */
protected String domain;
protected String name;
/** The name of the classification. */
@NotNull protected String name;
/** The Id of the parent classification. Empty string ("") if this is a root classification. */
protected String parentId;
/** The key of the parent classification. Empty string ("") if this is a root classification. */
protected String parentKey;
protected int priority;
protected String serviceLevel;
/** The priority of the classification. */
@NotNull protected int priority;
/**
* The service level of the classification.
*
* <p>This is stated according to ISO 8601.
*/
@NotNull protected String serviceLevel;
/** The type of classification. Types can be configured in the file 'taskana.properties'. */
protected String type;
/** A custom property with name "1". */
protected String custom1;
/** A custom property with name "2". */
protected String custom2;
/** A custom property with name "3". */
protected String custom3;
/** A custom property with name "4". */
protected String custom4;
/** A custom property with name "5". */
protected String custom5;
/** A custom property with name "6". */
protected String custom6;
/** A custom property with name "7". */
protected String custom7;
/** A custom property with name "8". */
protected String custom8;
public String getClassificationId() {

View File

@ -1,117 +0,0 @@
package pro.taskana.common.rest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.hateoas.PagedModel.PageMetadata;
import org.springframework.util.MultiValueMap;
import pro.taskana.common.api.BaseQuery;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
/** Abstract superclass for taskana REST controller with pageable resources. */
public abstract class AbstractPagingController {
private static final String PAGING_PAGE = "page";
private static final String PAGING_PAGE_SIZE = "page-size";
protected String[] extractCommaSeparatedFields(List<String> list) {
List<String> values = new ArrayList<>();
if (list != null) {
list.forEach(item -> values.addAll(Arrays.asList(item.split(","))));
}
return values.toArray(new String[0]);
}
protected void validateNoInvalidParameterIsLeft(MultiValueMap<String, String> params)
throws InvalidArgumentException {
if (!params.isEmpty()) {
throw new InvalidArgumentException("Invalid parameter specified: " + params.keySet());
}
}
protected PageMetadata getPageMetadata(
MultiValueMap<String, String> params, BaseQuery<?, ?> query) throws InvalidArgumentException {
PageMetadata pageMetadata = null;
if (hasPagingInformationInParams(params)) {
// paging
long totalElements = query.count();
pageMetadata = initPageMetadata(params, totalElements);
validateNoInvalidParameterIsLeft(params);
} else {
// not paging
validateNoInvalidParameterIsLeft(params);
}
return pageMetadata;
}
protected <T> List<T> getQueryList(BaseQuery<T, ?> query, PageMetadata pageMetadata) {
List<T> resultList;
if (pageMetadata != null) {
resultList = query.listPage((int) pageMetadata.getNumber(), (int) pageMetadata.getSize());
} else {
resultList = query.list();
}
return resultList;
}
protected PageMetadata initPageMetadata(MultiValueMap<String, String> param, long totalElements)
throws InvalidArgumentException {
long pageSize = getPageSize(param);
long page = getPage(param);
PageMetadata pageMetadata =
new PageMetadata(pageSize, page, totalElements >= 0 ? totalElements : Integer.MAX_VALUE);
if (pageMetadata.getNumber() > pageMetadata.getTotalPages()) {
// unfortunately no setter for number
pageMetadata = new PageMetadata(pageSize, pageMetadata.getTotalPages(), totalElements);
}
return pageMetadata;
}
// This method is deprecated please remove it after updating taskana-simple-history reference to
// it.
// TODO: @Deprecated
protected PageMetadata initPageMetadata(
String pagesizeParam, String pageParam, long totalElements) throws InvalidArgumentException {
long pageSize;
long page;
try {
pageSize = Long.parseLong(pagesizeParam);
page = Long.parseLong(pageParam);
} catch (NumberFormatException e) {
throw new InvalidArgumentException(
"page and pageSize must be a integer value.", e.getCause());
}
PageMetadata pageMetadata = new PageMetadata(pageSize, page, totalElements);
if (pageMetadata.getNumber() > pageMetadata.getTotalPages()) {
// unfortunately no setter for number
pageMetadata = new PageMetadata(pageSize, pageMetadata.getTotalPages(), totalElements);
}
return pageMetadata;
}
private boolean hasPagingInformationInParams(MultiValueMap<String, String> params) {
return params.getFirst(PAGING_PAGE) != null;
}
private long getPage(MultiValueMap<String, String> params) throws InvalidArgumentException {
String param = params.getFirst(PAGING_PAGE);
params.remove(PAGING_PAGE);
try {
return Long.parseLong(param != null ? param : "1");
} catch (NumberFormatException e) {
throw new InvalidArgumentException("page must be a integer value.", e.getCause());
}
}
private long getPageSize(MultiValueMap<String, String> params) throws InvalidArgumentException {
String param = params.getFirst(PAGING_PAGE_SIZE);
params.remove(PAGING_PAGE_SIZE);
try {
return param != null ? Long.parseLong(param) : Integer.MAX_VALUE;
} catch (NumberFormatException e) {
throw new InvalidArgumentException("page-size must be a integer value.", e.getCause());
}
}
}

View File

@ -18,7 +18,7 @@ import pro.taskana.common.api.exceptions.NotAuthorizedException;
import pro.taskana.common.rest.ldap.LdapClient;
import pro.taskana.common.rest.models.AccessIdRepresentationModel;
/** Controller for access id validation. */
/** Controller for Access Id validation. */
@RestController
@EnableHypermediaSupport(type = HypermediaType.HAL)
public class AccessIdController {
@ -34,8 +34,18 @@ public class AccessIdController {
this.taskanaEngine = taskanaEngine;
}
/**
* This endpoint searches a provided access Id in the configured ldap.
*
* @title Search for Access Id (users and groups)
* @param searchFor the Access Id which should be searched for.
* @return a list of all found Access Ids
* @throws InvalidArgumentException if the provided search for Access Id is shorter than the
* configured one.
* @throws NotAuthorizedException if the current user is not ADMIN or BUSINESS_ADMIN.
*/
@GetMapping(path = RestEndpoints.URL_ACCESS_ID)
public ResponseEntity<List<AccessIdRepresentationModel>> validateAccessIds(
public ResponseEntity<List<AccessIdRepresentationModel>> searchUsersAndGroups(
@RequestParam("search-for") String searchFor)
throws InvalidArgumentException, NotAuthorizedException {
@ -43,16 +53,8 @@ public class AccessIdController {
taskanaEngine.checkRoleMembership(TaskanaRole.ADMIN, TaskanaRole.BUSINESS_ADMIN);
if (searchFor.length() < ldapClient.getMinSearchForLength()) {
throw new InvalidArgumentException(
"searchFor string '"
+ searchFor
+ "' is too short. Minimum searchFor length = "
+ ldapClient.getMinSearchForLength());
}
ResponseEntity<List<AccessIdRepresentationModel>> response;
List<AccessIdRepresentationModel> accessIdUsers = ldapClient.searchUsersAndGroups(searchFor);
response = ResponseEntity.ok(accessIdUsers);
ResponseEntity<List<AccessIdRepresentationModel>> response = ResponseEntity.ok(accessIdUsers);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exit from validateAccessIds(), returning {}", response);
}
@ -60,6 +62,15 @@ public class AccessIdController {
return response;
}
/**
* This endpoint retrieves all groups a given Access Id belongs to.
*
* @title Get groups for Access Id
* @param accessId the Access Id whose groups should be determined.
* @return a list of the group Access Ids the requested Access Id belongs to
* @throws InvalidArgumentException if the requested Access Id does not exist or is not unique.
* @throws NotAuthorizedException if the current user is not ADMIN or BUSINESS_ADMIN.
*/
@GetMapping(path = RestEndpoints.URL_ACCESS_ID_GROUPS)
public ResponseEntity<List<AccessIdRepresentationModel>> getGroupsByAccessId(
@RequestParam("access-id") String accessId)

View File

@ -1,80 +0,0 @@
package pro.taskana.common.rest;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.MultiValueMap;
import pro.taskana.common.api.BaseQuery.SortDirection;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.internal.util.CheckedBiConsumer;
public class QueryHelper {
public static final String SORT_BY = "sort-by";
public static final String ORDER_DIRECTION = "order";
private static final Logger LOGGER = LoggerFactory.getLogger(QueryHelper.class);
private QueryHelper() {
// no op
}
public static void applyAndRemoveSortingParams(
MultiValueMap<String, String> params,
CheckedBiConsumer<String, SortDirection, InvalidArgumentException> consumer)
throws InvalidArgumentException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entry to applyAndRemoveSortingParams(params= {})", params);
}
if (params == null || consumer == null) {
throw new InvalidArgumentException("params or consumer can't be null!");
}
List<String> allSortBy = params.remove(SORT_BY);
List<String> allOrderBy = params.remove(ORDER_DIRECTION);
verifyNotOnlyOrderByExists(allSortBy, allOrderBy);
verifyAmountOfSortByAndOrderByMatches(allSortBy, allOrderBy);
if (allSortBy != null) {
for (int i = 0; i < allSortBy.size(); i++) {
consumer.accept(allSortBy.get(i), getSortDirectionForIndex(allOrderBy, i));
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exit from applyAndRemoveSortingParams()");
}
}
private static SortDirection getSortDirectionForIndex(List<String> allOrderBy, int i) {
SortDirection sortDirection = SortDirection.ASCENDING;
if (allOrderBy != null && !allOrderBy.isEmpty() && "desc".equalsIgnoreCase(allOrderBy.get(i))) {
sortDirection = SortDirection.DESCENDING;
}
return sortDirection;
}
private static void verifyNotOnlyOrderByExists(List<String> allSortBy, List<String> allOrderBy)
throws InvalidArgumentException {
if (allSortBy == null && allOrderBy != null) {
throw new InvalidArgumentException(
String.format(
"Only '%s' were provided. Please also provide '%s' parameter(s)",
ORDER_DIRECTION, SORT_BY));
}
}
private static void verifyAmountOfSortByAndOrderByMatches(
List<String> allSortBy, List<String> allOrderBy) throws InvalidArgumentException {
if (allSortBy != null
&& allOrderBy != null
&& allSortBy.size() != allOrderBy.size()
&& !allOrderBy.isEmpty()) {
throw new InvalidArgumentException(
String.format(
"The amount of '%s' and '%s' does not match. "
+ "Please specify an '%s' for each '%s' or no '%s' parameters at all.",
SORT_BY, ORDER_DIRECTION, ORDER_DIRECTION, SORT_BY, ORDER_DIRECTION));
}
}
}

View File

@ -0,0 +1,61 @@
package pro.taskana.common.rest;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.beans.ConstructorProperties;
import java.util.List;
import javax.validation.constraints.Min;
import pro.taskana.common.api.BaseQuery;
import pro.taskana.common.rest.models.PageMetadata;
public class QueryPagingParameter<T, Q extends BaseQuery<T, ?>>
implements QueryParameter<Q, List<T>> {
/** Request a specific page. Requires the definition of the 'page-size'. */
@Min(1)
private final Integer page;
/** Defines the size for each page. This requires a specific requested 'page'. */
@JsonProperty("page-size")
@Min(1)
private final Integer pageSize;
@JsonIgnore private PageMetadata pageMetadata;
@ConstructorProperties({"page", "page-size"})
public QueryPagingParameter(Integer page, Integer pageSize) {
// TODO: do we really want this? Personally I would throw an InvalidArgumentException
if (pageSize == null) {
pageSize = Integer.MAX_VALUE;
}
this.page = page;
this.pageSize = pageSize;
}
public PageMetadata getPageMetadata() {
return pageMetadata;
}
@Override
public List<T> applyToQuery(Q query) {
initPageMetaData(query);
List<T> resultList;
if (pageMetadata != null) {
resultList =
query.listPage(
Math.toIntExact(pageMetadata.getNumber()), Math.toIntExact(pageMetadata.getSize()));
} else {
resultList = query.list();
}
return resultList;
}
private void initPageMetaData(Q query) {
if (page != null) {
long totalElements = query.count();
long maxPages = (long) Math.ceil(totalElements / pageSize.doubleValue());
pageMetadata = new PageMetadata(pageSize, totalElements, maxPages, Math.min(page, maxPages));
}
}
}

View File

@ -0,0 +1,35 @@
package pro.taskana.common.rest;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import pro.taskana.common.api.BaseQuery;
import pro.taskana.common.api.TimeInterval;
public interface QueryParameter<Q extends BaseQuery<?, ?>, R> {
R applyToQuery(Q query);
default String[] wrapElementsInLikeStatement(String[] list) {
if (list != null) {
for (int i = 0; i < list.length; i++) {
list[i] = "%" + list[i] + "%";
}
}
return list;
}
default TimeInterval[] extractTimeIntervals(Instant[] instants) {
List<TimeInterval> timeIntervalsList = new ArrayList<>();
for (int i = 0; i < instants.length - 1; i += 2) {
Instant left = instants[i];
Instant right = instants[i + 1];
if (left != null || right != null) {
timeIntervalsList.add(new TimeInterval(left, right));
}
}
return timeIntervalsList.toArray(new TimeInterval[0]);
}
}

View File

@ -0,0 +1,9 @@
package pro.taskana.common.rest;
import pro.taskana.common.api.BaseQuery;
import pro.taskana.common.api.BaseQuery.SortDirection;
public interface QuerySortBy<Q extends BaseQuery<?, ?>> {
void applySortByForQuery(Q query, SortDirection sortDirection);
}

View File

@ -0,0 +1,80 @@
package pro.taskana.common.rest;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import pro.taskana.common.api.BaseQuery;
import pro.taskana.common.api.BaseQuery.SortDirection;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
public class QuerySortParameter<Q extends BaseQuery<?, ?>, S extends QuerySortBy<Q>>
implements QueryParameter<Q, Void> {
// the javadoc comment for this field is above its getter. This is done to define the type
// parameter S by overriding that getter and allowing spring-auto-rest-docs to properly detect
// the type parameter S.
private final List<S> sortBy;
/**
* The order direction for each sort value. This value requires the use of 'sort-by'. The amount
* of sort-by and order declarations have to match. Alternatively the value can be omitted. If
* done so the default sort order (ASCENDING) will be applied to every sort-by value.
*/
private final List<SortDirection> order;
// this is only necessary because spring-auto-rest-docs can't resolve Enum[] data types.
// See https://github.com/ScaCap/spring-auto-restdocs/issues/423
public QuerySortParameter(List<S> sortBy, List<SortDirection> order)
throws InvalidArgumentException {
this.sortBy = sortBy;
this.order = order;
verifyNotOnlyOrderByExists(sortBy, order);
verifyAmountOfSortByAndOrderByMatches(sortBy, order);
}
@Override
public Void applyToQuery(Q query) {
if (sortBy != null) {
for (int i = 0; i < sortBy.size(); i++) {
SortDirection sortDirection =
order == null || order.isEmpty() ? SortDirection.ASCENDING : order.get(i);
sortBy.get(i).applySortByForQuery(query, sortDirection);
}
}
return null;
}
// this method is only static because there exists no query for the task comment entity
public static <T> void verifyAmountOfSortByAndOrderByMatches(
List<T> sortBy, List<SortDirection> order) throws InvalidArgumentException {
if (sortBy != null && order != null && sortBy.size() != order.size() && order.size() > 0) {
throw new InvalidArgumentException(
"The amount of 'sort-by' and 'order' does not match. "
+ "Please specify an 'order' for each 'sort-by' or no 'order' parameters at all.");
}
}
// this method is only static because there exists no query for the task comment entity
public static <T> void verifyNotOnlyOrderByExists(List<T> sortBy, List<SortDirection> order)
throws InvalidArgumentException {
if (sortBy == null && order != null) {
throw new InvalidArgumentException(
"Only 'order' parameters were provided. Please also provide 'sort-by' parameter(s)");
}
}
/**
* Sort the result by a given field. Multiple sort values can be declared. When the primary sort
* value is the same, the second one will be used.
*
* @return the sort values
*/
@JsonProperty("sort-by")
public List<S> getSortBy() {
return sortBy;
}
public List<SortDirection> getOrder() {
return order;
}
}

View File

@ -13,6 +13,7 @@ public final class RestEndpoints {
public static final String URL_CLASSIFICATION_TYPES = API_V1 + "classification-types";
public static final String URL_CLASSIFICATION_CATEGORIES_BY_TYPES =
API_V1 + "classifications-by-type";
public static final String URL_HISTORY_ENABLED = API_V1 + "history-provider-enabled";
// access id endpoints
public static final String URL_ACCESS_ID = API_V1 + "access-ids";
@ -61,9 +62,5 @@ public final class RestEndpoints {
API_V1 + "monitor/tasks-classification-report";
public static final String URL_MONITOR_TIMESTAMP_REPORT = API_V1 + "monitor/timestamp-report";
public static final String URL_HISTORY_ENABLED = API_V1 + "history-provider-enabled";
public static final String URL_HISTORY_EVENTS = API_V1 + "task-history-event";
public static final String URL_HISTORY_EVENTS_ID = "/{historyEventId}";
private RestEndpoints() {}
}

View File

@ -1,4 +1,4 @@
package pro.taskana.rest.security;
package pro.taskana.common.rest;
import java.io.IOException;
import java.security.AccessController;
@ -60,10 +60,7 @@ public class SpringSecurityToJaasFilter extends GenericFilterBean {
if (logger.isDebugEnabled()) {
logger.debug("Attempting to obtainSubject using authentication : " + authentication);
}
if (!authentication.isPresent()) {
return Optional.empty();
}
if (!authentication.get().isAuthenticated()) {
if (!authentication.isPresent() || !authentication.get().isAuthenticated()) {
return Optional.empty();
}

View File

@ -4,10 +4,10 @@ import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import pro.taskana.TaskanaEngineConfiguration;
@ -27,15 +27,17 @@ public class TaskanaEngineController {
private final TaskanaEngine taskanaEngine;
@Value("${version:Local build}")
private String version;
TaskanaEngineController(
TaskanaEngineConfiguration taskanaEngineConfiguration, TaskanaEngine taskanaEngine) {
this.taskanaEngineConfiguration = taskanaEngineConfiguration;
this.taskanaEngine = taskanaEngine;
}
/**
* This endpoint retrieves all configured Domains.
*
* @return An array with the domain-names as strings
*/
@GetMapping(path = RestEndpoints.URL_DOMAIN)
public ResponseEntity<List<String>> getDomains() {
ResponseEntity<List<String>> response =
@ -46,8 +48,17 @@ public class TaskanaEngineController {
return response;
}
/**
* This endpoint retrieves the configured classification categories for a specific classification
* type.
*
* @param type the classification type whose categories should be determined. If not specified all
* classification categories will be returned.
* @return the classification categories for the requested type.
*/
@GetMapping(path = RestEndpoints.URL_CLASSIFICATION_CATEGORIES)
public ResponseEntity<List<String>> getClassificationCategories(String type) {
public ResponseEntity<List<String>> getClassificationCategories(
@RequestParam(required = false) String type) {
LOGGER.debug("Entry to getClassificationCategories(type = {})", type);
ResponseEntity<List<String>> response;
if (type != null) {
@ -65,6 +76,11 @@ public class TaskanaEngineController {
return response;
}
/**
* This endpoint retrieves the configured classification types.
*
* @return the configured classification types.
*/
@GetMapping(path = RestEndpoints.URL_CLASSIFICATION_TYPES)
public ResponseEntity<List<String>> getClassificationTypes() {
ResponseEntity<List<String>> response =
@ -75,6 +91,12 @@ public class TaskanaEngineController {
return response;
}
/**
* This endpoint retrieves all configured classification categories grouped by each classification
* type.
*
* @return the configured classification categories
*/
@GetMapping(path = RestEndpoints.URL_CLASSIFICATION_CATEGORIES_BY_TYPES)
public ResponseEntity<Map<String, List<String>>> getClassificationCategoriesByTypeMap() {
ResponseEntity<Map<String, List<String>>> response =
@ -85,6 +107,11 @@ public class TaskanaEngineController {
return response;
}
/**
* This endpoint computes all information of the current user.
*
* @return the information of the current user.
*/
@GetMapping(path = RestEndpoints.URL_CURRENT_USER)
public ResponseEntity<TaskanaUserInfoRepresentationModel> getCurrentUserInfo() {
LOGGER.debug("Entry to getCurrentUserInfo()");
@ -104,6 +131,11 @@ public class TaskanaEngineController {
return response;
}
/**
* This endpoint checks if the history module is in use.
*
* @return true, when the history is enabled, otherwise false
*/
@GetMapping(path = RestEndpoints.URL_HISTORY_ENABLED)
public ResponseEntity<Boolean> getIsHistoryProviderEnabled() {
ResponseEntity<Boolean> response = ResponseEntity.ok(taskanaEngine.isHistoryEnabled());

View File

@ -17,6 +17,7 @@ import pro.taskana.common.api.exceptions.DomainNotFoundException;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.api.exceptions.NotAuthorizedException;
import pro.taskana.common.api.exceptions.NotFoundException;
import pro.taskana.common.rest.models.TaskanaErrorData;
import pro.taskana.task.api.exceptions.InvalidOwnerException;
import pro.taskana.task.api.exceptions.InvalidStateException;
import pro.taskana.task.api.exceptions.TaskAlreadyExistException;

Some files were not shown because too many files have changed in this diff Show More