diff --git a/common/taskana-common-test/src/main/java/pro/taskana/common/test/rest/TaskanaSpringBootTest.java b/common/taskana-common-test/src/main/java/pro/taskana/common/test/rest/TaskanaSpringBootTest.java index 94c602ac8..6d40f3e74 100644 --- a/common/taskana-common-test/src/main/java/pro/taskana/common/test/rest/TaskanaSpringBootTest.java +++ b/common/taskana-common-test/src/main/java/pro/taskana/common/test/rest/TaskanaSpringBootTest.java @@ -1,7 +1,6 @@ package pro.taskana.common.test.rest; import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -16,7 +15,6 @@ import org.springframework.test.context.ActiveProfiles; // DirtiesContext is required to make the integration tests run with embedded LDAP. // Otherwise the LDAP server is not shut down correctly and will not come up again. (socket busy) @DirtiesContext(classMode = ClassMode.AFTER_CLASS) -@Inherited @ActiveProfiles({"test"}) @SpringBootTest( classes = TestConfiguration.class, diff --git a/lib/taskana-core/src/main/java/pro/taskana/monitor/api/reports/header/PriorityColumnHeader.java b/lib/taskana-core/src/main/java/pro/taskana/monitor/api/reports/header/PriorityColumnHeader.java index bd18de972..46a898861 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/monitor/api/reports/header/PriorityColumnHeader.java +++ b/lib/taskana-core/src/main/java/pro/taskana/monitor/api/reports/header/PriorityColumnHeader.java @@ -27,4 +27,12 @@ public class PriorityColumnHeader implements ColumnHeader { public boolean fits(PriorityQueryItem item) { return lowerBoundInc <= item.getPriority() && upperBoundInc >= item.getPriority(); } + + public int getLowerBoundInc() { + return lowerBoundInc; + } + + public int getUpperBoundInc() { + return upperBoundInc; + } } diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/JsonPropertyEditor.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/JsonPropertyEditor.java new file mode 100644 index 000000000..2190f281e --- /dev/null +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/JsonPropertyEditor.java @@ -0,0 +1,27 @@ +package pro.taskana.common.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.beans.PropertyEditorSupport; +import java.net.URLDecoder; + +public class JsonPropertyEditor extends PropertyEditorSupport { + + private final ObjectMapper objectMapper; + private final Class requiredType; + + public JsonPropertyEditor(ObjectMapper objectMapper, Class requiredType) { + this.objectMapper = objectMapper; + this.requiredType = requiredType; + } + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (text != null && !text.isEmpty()) { + try { + setValue(objectMapper.readValue(URLDecoder.decode(text, "UTF-8"), requiredType)); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + } +} diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/JsonPropertyEditorRegistrator.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/JsonPropertyEditorRegistrator.java new file mode 100644 index 000000000..c3afd3564 --- /dev/null +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/JsonPropertyEditorRegistrator.java @@ -0,0 +1,27 @@ +package pro.taskana.common.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.InitBinder; + +import pro.taskana.monitor.rest.models.PriorityColumnHeaderRepresentationModel; + +@ControllerAdvice +public class JsonPropertyEditorRegistrator { + + private final ObjectMapper objectMapper; + + @Autowired + public JsonPropertyEditorRegistrator(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @InitBinder + public void initBinder(WebDataBinder binder) { + binder.registerCustomEditor( + PriorityColumnHeaderRepresentationModel.class, + new JsonPropertyEditor(objectMapper, PriorityColumnHeaderRepresentationModel.class)); + } +} diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/MonitorController.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/MonitorController.java index 76775cb45..817973843 100644 --- a/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/MonitorController.java +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/MonitorController.java @@ -3,6 +3,7 @@ package pro.taskana.monitor.rest; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.http.HttpStatus; @@ -24,7 +25,9 @@ import pro.taskana.monitor.api.reports.TimestampReport; import pro.taskana.monitor.api.reports.WorkbasketPriorityReport; import pro.taskana.monitor.api.reports.WorkbasketReport; import pro.taskana.monitor.api.reports.header.PriorityColumnHeader; +import pro.taskana.monitor.rest.assembler.PriorityColumnHeaderRepresentationModelAssembler; import pro.taskana.monitor.rest.assembler.ReportRepresentationModelAssembler; +import pro.taskana.monitor.rest.models.PriorityColumnHeaderRepresentationModel; import pro.taskana.monitor.rest.models.ReportRepresentationModel; import pro.taskana.task.api.TaskCustomField; import pro.taskana.task.api.TaskState; @@ -38,13 +41,19 @@ public class MonitorController { private final MonitorService monitorService; private final ReportRepresentationModelAssembler reportRepresentationModelAssembler; + private final PriorityColumnHeaderRepresentationModelAssembler + priorityColumnHeaderRepresentationModelAssembler; @Autowired MonitorController( MonitorService monitorService, - ReportRepresentationModelAssembler reportRepresentationModelAssembler) { + ReportRepresentationModelAssembler reportRepresentationModelAssembler, + PriorityColumnHeaderRepresentationModelAssembler + priorityColumnHeaderRepresentationModelAssembler) { this.monitorService = monitorService; this.reportRepresentationModelAssembler = reportRepresentationModelAssembler; + this.priorityColumnHeaderRepresentationModelAssembler = + priorityColumnHeaderRepresentationModelAssembler; } /** @@ -82,23 +91,15 @@ public class MonitorController { } /** - * This endpoint generates a Workbasket Report by priority ranges. + * This endpoint generates a Workbasket Priority Report. * *

Each Row represents a Workbasket. * - *

Each Column Header represents a priority range.
- *
- * - *

Default ranges - * - *

High: priority > 500 - * - *

Medium: 250 ≥ priority ≤ 500 - * - *

Low: priority < 250 + *

Each Column Header represents a priority range. * * @title Compute a Workbasket Priority Report * @param workbasketTypes determine the WorkbasketTypes to include in the report + * @param columnHeaders the column headers for the report * @return the computed Report * @throws NotAuthorizedException if the current user is not authorized to compute the Report * @throws InvalidArgumentException if topicWorkbaskets or useDefaultValues are false @@ -106,21 +107,25 @@ public class MonitorController { @GetMapping(path = RestEndpoints.URL_MONITOR_WORKBASKET_PRIORITY_REPORT) @Transactional(readOnly = true, rollbackFor = Exception.class) public ResponseEntity computePriorityWorkbasketReport( - @RequestParam(name = "workbasket-type", required = false) WorkbasketType[] workbasketTypes) + @RequestParam(name = "workbasket-type", required = false) WorkbasketType[] workbasketTypes, + @RequestParam(name = "columnHeader", required = false) + PriorityColumnHeaderRepresentationModel[] columnHeaders) throws NotAuthorizedException, InvalidArgumentException { WorkbasketPriorityReport.Builder builder = - monitorService - .createWorkbasketPriorityReportBuilder() - .withColumnHeaders( - Arrays.asList( - new PriorityColumnHeader(Integer.MIN_VALUE, 249), - new PriorityColumnHeader(250, 500), - new PriorityColumnHeader(501, Integer.MAX_VALUE))) - .workbasketTypeIn(workbasketTypes); + monitorService.createWorkbasketPriorityReportBuilder().workbasketTypeIn(workbasketTypes); + + if (columnHeaders != null) { + List priorityColumnHeaders = + Arrays.stream(columnHeaders) + .map(priorityColumnHeaderRepresentationModelAssembler::toEntityModel) + .collect(Collectors.toList()); + builder.withColumnHeaders(priorityColumnHeaders); + } ReportRepresentationModel report = - reportRepresentationModelAssembler.toModel(builder.buildReport(), workbasketTypes); + reportRepresentationModelAssembler.toModel( + builder.buildReport(), workbasketTypes, columnHeaders); return ResponseEntity.status(HttpStatus.OK).body(report); } diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/assembler/PriorityColumnHeaderRepresentationModelAssembler.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/assembler/PriorityColumnHeaderRepresentationModelAssembler.java new file mode 100644 index 000000000..28dd6060f --- /dev/null +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/assembler/PriorityColumnHeaderRepresentationModelAssembler.java @@ -0,0 +1,25 @@ +package pro.taskana.monitor.rest.assembler; + +import org.springframework.hateoas.server.RepresentationModelAssembler; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +import pro.taskana.monitor.api.reports.header.PriorityColumnHeader; +import pro.taskana.monitor.rest.models.PriorityColumnHeaderRepresentationModel; + +@Component +public class PriorityColumnHeaderRepresentationModelAssembler + implements RepresentationModelAssembler< + PriorityColumnHeader, PriorityColumnHeaderRepresentationModel> { + + @Override + @NonNull + public PriorityColumnHeaderRepresentationModel toModel(@NonNull PriorityColumnHeader entity) { + return new PriorityColumnHeaderRepresentationModel( + entity.getLowerBoundInc(), entity.getUpperBoundInc()); + } + + public PriorityColumnHeader toEntityModel(PriorityColumnHeaderRepresentationModel repModel) { + return new PriorityColumnHeader(repModel.getLowerBound(), repModel.getUpperBound()); + } +} diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/assembler/ReportRepresentationModelAssembler.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/assembler/ReportRepresentationModelAssembler.java index e441ab7ee..98b277c8a 100644 --- a/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/assembler/ReportRepresentationModelAssembler.java +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/assembler/ReportRepresentationModelAssembler.java @@ -32,6 +32,7 @@ import pro.taskana.monitor.api.reports.row.Row; import pro.taskana.monitor.api.reports.row.SingleRow; import pro.taskana.monitor.rest.MonitorController; import pro.taskana.monitor.rest.TimeIntervalReportFilterParameter; +import pro.taskana.monitor.rest.models.PriorityColumnHeaderRepresentationModel; import pro.taskana.monitor.rest.models.ReportRepresentationModel; import pro.taskana.monitor.rest.models.ReportRepresentationModel.RowRepresentationModel; import pro.taskana.task.api.TaskCustomField; @@ -59,11 +60,15 @@ public class ReportRepresentationModelAssembler { @NonNull public ReportRepresentationModel toModel( - @NonNull WorkbasketPriorityReport report, @NonNull WorkbasketType[] workbasketTypes) - throws NotAuthorizedException, InvalidArgumentException { + @NonNull WorkbasketPriorityReport report, + WorkbasketType[] workbasketTypes, + PriorityColumnHeaderRepresentationModel[] columnHeaders) + throws InvalidArgumentException, NotAuthorizedException { ReportRepresentationModel resource = toReportResource(report); resource.add( - linkTo(methodOn(MonitorController.class).computePriorityWorkbasketReport(workbasketTypes)) + linkTo( + methodOn(MonitorController.class) + .computePriorityWorkbasketReport(workbasketTypes, columnHeaders)) .withSelfRel()); return resource; } diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/models/PriorityColumnHeaderRepresentationModel.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/models/PriorityColumnHeaderRepresentationModel.java new file mode 100644 index 000000000..3ef791851 --- /dev/null +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/monitor/rest/models/PriorityColumnHeaderRepresentationModel.java @@ -0,0 +1,30 @@ +package pro.taskana.monitor.rest.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.beans.ConstructorProperties; +import org.springframework.hateoas.RepresentationModel; + +@JsonIgnoreProperties("links") +public class PriorityColumnHeaderRepresentationModel + extends RepresentationModel { + + /** Determine the lower priority for this column header. This value is inclusive. */ + private final int lowerBound; + + /** Determine the upper priority for this column header. This value is inclusive. */ + private final int upperBound; + + @ConstructorProperties({"lowerBound", "upperBound"}) + public PriorityColumnHeaderRepresentationModel(int lowerBound, int upperBound) { + this.lowerBound = lowerBound; + this.upperBound = upperBound; + } + + public int getLowerBound() { + return lowerBound; + } + + public int getUpperBound() { + return upperBound; + } +} diff --git a/rest/taskana-rest-spring/src/test/java/pro/taskana/monitor/rest/MonitorControllerIntTest.java b/rest/taskana-rest-spring/src/test/java/pro/taskana/monitor/rest/MonitorControllerIntTest.java index 33d2af219..8c88f940a 100644 --- a/rest/taskana-rest-spring/src/test/java/pro/taskana/monitor/rest/MonitorControllerIntTest.java +++ b/rest/taskana-rest-spring/src/test/java/pro/taskana/monitor/rest/MonitorControllerIntTest.java @@ -1,8 +1,13 @@ package pro.taskana.monitor.rest; 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 com.fasterxml.jackson.databind.ObjectMapper; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; @@ -10,11 +15,12 @@ import org.springframework.hateoas.IanaLinkRelations; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; +import org.springframework.web.client.HttpClientErrorException.BadRequest; -import pro.taskana.common.api.TaskanaEngine; import pro.taskana.common.rest.RestEndpoints; import pro.taskana.common.test.rest.RestHelper; import pro.taskana.common.test.rest.TaskanaSpringBootTest; +import pro.taskana.monitor.rest.models.PriorityColumnHeaderRepresentationModel; import pro.taskana.monitor.rest.models.ReportRepresentationModel; import pro.taskana.monitor.rest.models.ReportRepresentationModel.RowRepresentationModel; @@ -23,12 +29,12 @@ import pro.taskana.monitor.rest.models.ReportRepresentationModel.RowRepresentati class MonitorControllerIntTest { private final RestHelper restHelper; - private final TaskanaEngine taskanaEngine; + private final ObjectMapper objectMapper; @Autowired - MonitorControllerIntTest(RestHelper restHelper, TaskanaEngine taskanaEngine) { + MonitorControllerIntTest(RestHelper restHelper, ObjectMapper objectMapper) { this.restHelper = restHelper; - this.taskanaEngine = taskanaEngine; + this.objectMapper = objectMapper; } @Test @@ -106,10 +112,10 @@ class MonitorControllerIntTest { } @Test - void should_ComputeReport_When_QueryingForAWorkbasketPriorityReport() { + void should_ComputeWorkbasketPriorityReport_When_QueryingForAWorkbasketPriorityReport() { String url = restHelper.toUrl(RestEndpoints.URL_MONITOR_WORKBASKET_PRIORITY_REPORT) - + "?workbasket-type=TOPIC,GROUP"; + + "?workbasket-type=TOPIC&workbasket-type=GROUP"; HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("monitor")); ResponseEntity response = @@ -123,8 +129,50 @@ class MonitorControllerIntTest { assertThat(report).isNotNull(); - assertThat(report.getSumRow()) - .extracting(RowRepresentationModel::getCells) - .containsExactly(new int[] {25, 1, 0}); + assertThat(report.getSumRow()).extracting(RowRepresentationModel::getTotal).containsExactly(26); + } + + @Test + void should_DetectPriorityColumnHeader_When_HeaderIsPassedAsQueryParameter() throws Exception { + PriorityColumnHeaderRepresentationModel columnHeader = + new PriorityColumnHeaderRepresentationModel(10, 20); + + String url = + restHelper.toUrl(RestEndpoints.URL_MONITOR_WORKBASKET_PRIORITY_REPORT) + + "?columnHeader=" + + URLEncoder.encode( + objectMapper.writeValueAsString(columnHeader), StandardCharsets.UTF_8); + + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("monitor")); + + ResponseEntity response = + TEMPLATE.exchange( + url, + HttpMethod.GET, + auth, + ParameterizedTypeReference.forType(ReportRepresentationModel.class)); + + ReportRepresentationModel report = response.getBody(); + assertThat(report).isNotNull(); + assertThat(report.getMeta().getHeader()).containsExactly("10 - 20"); + } + + @Test + void should_ReturnBadRequest_When_PriorityColumnHeaderIsNotAValidJson() { + String url = + restHelper.toUrl(RestEndpoints.URL_MONITOR_WORKBASKET_PRIORITY_REPORT) + + "?columnHeader=invalidJson"; + + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("monitor")); + + ThrowingCallable httpCall = + () -> + TEMPLATE.exchange( + url, + HttpMethod.GET, + auth, + ParameterizedTypeReference.forType(ReportRepresentationModel.class)); + + assertThatThrownBy(httpCall).isInstanceOf(BadRequest.class); } } diff --git a/rest/taskana-rest-spring/src/test/java/pro/taskana/monitor/rest/assembler/PriorityColumnHeaderRepresentationModelAssemblerTest.java b/rest/taskana-rest-spring/src/test/java/pro/taskana/monitor/rest/assembler/PriorityColumnHeaderRepresentationModelAssemblerTest.java new file mode 100644 index 000000000..299480e84 --- /dev/null +++ b/rest/taskana-rest-spring/src/test/java/pro/taskana/monitor/rest/assembler/PriorityColumnHeaderRepresentationModelAssemblerTest.java @@ -0,0 +1,44 @@ +package pro.taskana.monitor.rest.assembler; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import pro.taskana.common.test.rest.TaskanaSpringBootTest; +import pro.taskana.monitor.api.reports.header.PriorityColumnHeader; +import pro.taskana.monitor.rest.models.PriorityColumnHeaderRepresentationModel; + +@TaskanaSpringBootTest +class PriorityColumnHeaderRepresentationModelAssemblerTest { + + private final PriorityColumnHeaderRepresentationModelAssembler assembler; + + @Autowired + public PriorityColumnHeaderRepresentationModelAssemblerTest( + PriorityColumnHeaderRepresentationModelAssembler assembler) { + this.assembler = assembler; + } + + @Test + void should_convertEntityToRepresentationModel() { + PriorityColumnHeader columnHeader = new PriorityColumnHeader(10, 20); + PriorityColumnHeaderRepresentationModel expectedRepModel = + new PriorityColumnHeaderRepresentationModel(10, 20); + + PriorityColumnHeaderRepresentationModel repModel = assembler.toModel(columnHeader); + + assertThat(repModel).usingRecursiveComparison().isEqualTo(expectedRepModel); + } + + @Test + void should_convertRepresentationModelToEntity() { + PriorityColumnHeaderRepresentationModel repModel = + new PriorityColumnHeaderRepresentationModel(10, 20); + PriorityColumnHeader expectedColumnHeader = new PriorityColumnHeader(10, 20); + + PriorityColumnHeader columnHeader = assembler.toEntityModel(repModel); + + assertThat(columnHeader).usingRecursiveComparison().isEqualTo(expectedColumnHeader); + } +}