TSK-1941: implemented ReviewRequired SPI

This commit is contained in:
Mustapha Zorgati 2022-08-11 00:59:44 +02:00
parent 830723800e
commit cf690bf6b5
9 changed files with 271 additions and 8 deletions

View File

@ -0,0 +1,144 @@
package acceptance.task.complete;
import static org.assertj.core.api.Assertions.assertThat;
import static pro.taskana.testapi.DefaultTestEntities.defaultTestClassification;
import static pro.taskana.testapi.DefaultTestEntities.defaultTestWorkbasket;
import java.time.Instant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import pro.taskana.classification.api.ClassificationService;
import pro.taskana.classification.api.models.ClassificationSummary;
import pro.taskana.spi.task.api.ReviewRequiredProvider;
import pro.taskana.task.api.TaskService;
import pro.taskana.task.api.TaskState;
import pro.taskana.task.api.models.ObjectReference;
import pro.taskana.task.api.models.Task;
import pro.taskana.testapi.DefaultTestEntities;
import pro.taskana.testapi.TaskanaInject;
import pro.taskana.testapi.TaskanaIntegrationTest;
import pro.taskana.testapi.WithServiceProvider;
import pro.taskana.testapi.builder.TaskBuilder;
import pro.taskana.testapi.builder.WorkbasketAccessItemBuilder;
import pro.taskana.testapi.security.WithAccessId;
import pro.taskana.workbasket.api.WorkbasketPermission;
import pro.taskana.workbasket.api.WorkbasketService;
import pro.taskana.workbasket.api.models.WorkbasketSummary;
@TaskanaIntegrationTest
class CompleteTaskWithSpiAccTest {
@TaskanaInject TaskService taskService;
ClassificationSummary defaultClassificationSummary;
WorkbasketSummary defaultWorkbasketSummary;
ObjectReference defaultObjectReference;
@SuppressWarnings("JUnitMalformedDeclaration")
@WithAccessId(user = "businessadmin")
@BeforeAll
void setup(ClassificationService classificationService, WorkbasketService workbasketService)
throws Exception {
defaultClassificationSummary =
defaultTestClassification().buildAndStoreAsSummary(classificationService);
defaultWorkbasketSummary = defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService);
WorkbasketAccessItemBuilder.newWorkbasketAccessItem()
.workbasketId(defaultWorkbasketSummary.getId())
.accessId("user-1-1")
.permission(WorkbasketPermission.READ)
.permission(WorkbasketPermission.APPEND)
.buildAndStore(workbasketService);
defaultObjectReference = DefaultTestEntities.defaultTestObjectReference().build();
}
private TaskBuilder createTaskClaimedByUser(String user) {
return createDefaultTask().owner(user).state(TaskState.CLAIMED).claimed(Instant.now());
}
private TaskBuilder createDefaultTask() {
return TaskBuilder.newTask()
.classificationSummary(defaultClassificationSummary)
.workbasketSummary(defaultWorkbasketSummary)
.primaryObjRef(defaultObjectReference);
}
static class AlwaysRequireReview implements ReviewRequiredProvider {
@Override
public boolean reviewRequired(Task task) {
return true;
}
}
static class NeverRequireReview implements ReviewRequiredProvider {
@Override
public boolean reviewRequired(Task task) {
return false;
}
}
@Nested
@TestInstance(Lifecycle.PER_CLASS)
@WithServiceProvider(
serviceProviderInterface = ReviewRequiredProvider.class,
serviceProviders = AlwaysRequireReview.class)
class ServiceProviderAlwaysRequiringReview {
@TaskanaInject TaskService taskService;
@WithAccessId(user = "user-1-1")
@Test
void should_RequireReview_When_UserTriesToCompleteTask() throws Exception {
Task task = createTaskClaimedByUser("user-1-1").buildAndStore(taskService);
Task processedTask = taskService.completeTask(task.getId());
assertThat(processedTask.getState()).isEqualTo(TaskState.READY_FOR_REVIEW);
}
}
@Nested
@TestInstance(Lifecycle.PER_CLASS)
@WithServiceProvider(
serviceProviderInterface = ReviewRequiredProvider.class,
serviceProviders = NeverRequireReview.class)
class ServiceProviderNeverRequiringReview {
@TaskanaInject TaskService taskService;
@WithAccessId(user = "user-1-1")
@Test
void should_CompleteTask_When_UserTriesToCompleteTask() throws Exception {
Task task = createTaskClaimedByUser("user-1-1").buildAndStore(taskService);
Task processedTask = taskService.completeTask(task.getId());
assertThat(processedTask.getState()).isEqualTo(TaskState.COMPLETED);
}
}
@Nested
@TestInstance(Lifecycle.PER_CLASS)
@WithServiceProvider(
serviceProviderInterface = ReviewRequiredProvider.class,
serviceProviders = {AlwaysRequireReview.class, NeverRequireReview.class})
class ServiceProvidersAlwaysAndNeverRequiringReview {
@TaskanaInject TaskService taskService;
@WithAccessId(user = "user-1-1")
@Test
void should_RequestReview_When_UserTriesToCompleteTask() throws Exception {
Task task = createTaskClaimedByUser("user-1-1").buildAndStore(taskService);
Task processedTask = taskService.completeTask(task.getId());
assertThat(processedTask.getState()).isEqualTo(TaskState.READY_FOR_REVIEW);
}
}
}

View File

@ -88,11 +88,17 @@ public class RequestChangesWithSpiAccTest {
static class ExceptionThrower implements AfterRequestChangesProvider {
@Override
public void initialize(TaskanaEngine taskanaEngine) {}
private TaskanaEngine taskanaEngine;
@Override
public Task afterRequestChanges(Task task) {
public void initialize(TaskanaEngine taskanaEngine) {
this.taskanaEngine = taskanaEngine;
}
@Override
public Task afterRequestChanges(Task task) throws Exception {
task.setDescription("should not matter. Will get rolled back anyway");
taskanaEngine.getTaskService().updateTask(task);
throw new SystemException("I AM THE EXCEPTION THROWER (*_*)");
}
}

View File

@ -88,11 +88,17 @@ public class RequestReviewWithSpiAccTest {
static class ExceptionThrower implements AfterRequestReviewProvider {
@Override
public void initialize(TaskanaEngine taskanaEngine) {}
private TaskanaEngine taskanaEngine;
@Override
public Task afterRequestReview(Task task) {
public void initialize(TaskanaEngine taskanaEngine) {
this.taskanaEngine = taskanaEngine;
}
@Override
public Task afterRequestReview(Task task) throws Exception {
task.setDescription("should not matter. Will get rolled back anyway");
taskanaEngine.getTaskService().updateTask(task);
throw new SystemException("I AM THE EXCEPTION THROWER (*_*)");
}
}

View File

@ -10,6 +10,7 @@ import pro.taskana.spi.routing.internal.TaskRoutingManager;
import pro.taskana.spi.task.internal.AfterRequestChangesManager;
import pro.taskana.spi.task.internal.AfterRequestReviewManager;
import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager;
import pro.taskana.spi.task.internal.ReviewRequiredManager;
/**
* FOR INTERNAL USE ONLY.
@ -108,6 +109,13 @@ public interface InternalTaskanaEngine {
*/
PriorityServiceManager getPriorityServiceManager();
/**
* Retrieves the {@linkplain ReviewRequiredManager}.
*
* @return the {@linkplain ReviewRequiredManager} instance
*/
ReviewRequiredManager getReviewRequiredManager();
/**
* Retrieves the {@linkplain AfterRequestReviewManager}.
*

View File

@ -56,6 +56,7 @@ import pro.taskana.spi.routing.internal.TaskRoutingManager;
import pro.taskana.spi.task.internal.AfterRequestChangesManager;
import pro.taskana.spi.task.internal.AfterRequestReviewManager;
import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager;
import pro.taskana.spi.task.internal.ReviewRequiredManager;
import pro.taskana.task.api.TaskService;
import pro.taskana.task.internal.AttachmentMapper;
import pro.taskana.task.internal.ObjectReferenceMapper;
@ -84,6 +85,7 @@ public class TaskanaEngineImpl implements TaskanaEngine {
private final TaskRoutingManager taskRoutingManager;
private final CreateTaskPreprocessorManager createTaskPreprocessorManager;
private final PriorityServiceManager priorityServiceManager;
private final ReviewRequiredManager reviewRequiredManager;
private final AfterRequestReviewManager afterRequestReviewManager;
private final AfterRequestChangesManager afterRequestChangesManager;
@ -122,6 +124,7 @@ public class TaskanaEngineImpl implements TaskanaEngine {
createTaskPreprocessorManager = new CreateTaskPreprocessorManager();
historyEventManager = new HistoryEventManager(this);
taskRoutingManager = new TaskRoutingManager(this);
reviewRequiredManager = new ReviewRequiredManager(this);
afterRequestReviewManager = new AfterRequestReviewManager(this);
afterRequestChangesManager = new AfterRequestChangesManager(this);
}
@ -529,6 +532,11 @@ public class TaskanaEngineImpl implements TaskanaEngine {
return priorityServiceManager;
}
@Override
public ReviewRequiredManager getReviewRequiredManager() {
return reviewRequiredManager;
}
@Override
public AfterRequestReviewManager getAfterRequestReviewManager() {
return afterRequestReviewManager;

View File

@ -0,0 +1,43 @@
package pro.taskana.spi.task.api;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.task.api.models.Task;
/**
* The ReviewRequiredProvider allows to determine whether a {@linkplain Task} requires a review
* instead of completion.
*/
public interface ReviewRequiredProvider {
/**
* Provide the active {@linkplain TaskanaEngine} which is initialized for this TASKANA
* installation.
*
* <p>This method is called during TASKANA startup and allows the service provider to store the
* active {@linkplain TaskanaEngine} for later usage.
*
* @param taskanaEngine the active {@linkplain TaskanaEngine} which is initialized for this
* installation
*/
default void initialize(TaskanaEngine taskanaEngine) {}
/**
* Determine if a {@linkplain Task} has to be reviewed instead of completed before {@linkplain
* pro.taskana.task.api.TaskService#completeTask(String)} is executed.
*
* <p>The behaviour is undefined if this method tries to apply persistent changes to any entity.
*
* <p>This SPI is executed with the same {@linkplain
* pro.taskana.common.api.security.UserPrincipal} and {@linkplain
* pro.taskana.common.api.security.GroupPrincipal} as in {@linkplain
* pro.taskana.task.api.TaskService#completeTask(String)}.
*
* @param task the {@linkplain Task} before {@linkplain
* pro.taskana.task.api.TaskService#completeTask(String)} has started
* @return true, if {@linkplain pro.taskana.task.api.TaskService#requestReview(String)} should be
* executed instead of {@linkplain pro.taskana.task.api.TaskService#completeTask(String)}.
* False, if {@linkplain pro.taskana.task.api.TaskService#completeTask(String)} can be
* executed regularly
*/
boolean reviewRequired(Task task);
}

View File

@ -0,0 +1,41 @@
package pro.taskana.spi.task.internal;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.internal.util.SpiLoader;
import pro.taskana.spi.task.api.ReviewRequiredProvider;
import pro.taskana.task.api.models.Task;
public class ReviewRequiredManager {
private static final Logger LOGGER = LoggerFactory.getLogger(ReviewRequiredManager.class);
private final List<ReviewRequiredProvider> reviewRequiredProviders;
public ReviewRequiredManager(TaskanaEngine taskanaEngine) {
reviewRequiredProviders = SpiLoader.load(ReviewRequiredProvider.class);
for (ReviewRequiredProvider serviceProvider : reviewRequiredProviders) {
serviceProvider.initialize(taskanaEngine);
LOGGER.info(
"Registered ReviewRequiredProvider service provider: {}",
serviceProvider.getClass().getName());
}
if (reviewRequiredProviders.isEmpty()) {
LOGGER.info(
"No ReviewRequiredProvider service provider found. "
+ "Running without any ReviewRequiredProvider implementation.");
}
}
public boolean reviewRequired(Task task) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Sending Task to ReviewRequiredProvider service providers: {}", task);
}
return reviewRequiredProviders.stream()
.anyMatch(serviceProvider -> serviceProvider.reviewRequired(task));
}
}

View File

@ -56,6 +56,7 @@ import pro.taskana.spi.priority.internal.PriorityServiceManager;
import pro.taskana.spi.task.internal.AfterRequestChangesManager;
import pro.taskana.spi.task.internal.AfterRequestReviewManager;
import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager;
import pro.taskana.spi.task.internal.ReviewRequiredManager;
import pro.taskana.task.api.CallbackState;
import pro.taskana.task.api.TaskCommentQuery;
import pro.taskana.task.api.TaskCustomField;
@ -116,6 +117,7 @@ public class TaskServiceImpl implements TaskService {
private final HistoryEventManager historyEventManager;
private final CreateTaskPreprocessorManager createTaskPreprocessorManager;
private final PriorityServiceManager priorityServiceManager;
private final ReviewRequiredManager reviewRequiredManager;
private final AfterRequestReviewManager afterRequestReviewManager;
private final AfterRequestChangesManager afterRequestChangesManager;
@ -136,6 +138,7 @@ public class TaskServiceImpl implements TaskService {
this.historyEventManager = taskanaEngine.getHistoryEventManager();
this.createTaskPreprocessorManager = taskanaEngine.getCreateTaskPreprocessorManager();
this.priorityServiceManager = taskanaEngine.getPriorityServiceManager();
this.reviewRequiredManager = taskanaEngine.getReviewRequiredManager();
this.afterRequestReviewManager = taskanaEngine.getAfterRequestReviewManager();
this.afterRequestChangesManager = taskanaEngine.getAfterRequestChangesManager();
this.taskTransferrer = new TaskTransferrer(taskanaEngine, taskMapper, this);
@ -1448,10 +1451,12 @@ public class TaskServiceImpl implements TaskService {
throws TaskNotFoundException, InvalidOwnerException, InvalidStateException,
NotAuthorizedException {
String userId = taskanaEngine.getEngine().getCurrentUserContext().getUserid();
TaskImpl task;
TaskImpl task = (TaskImpl) this.getTask(taskId);
if (reviewRequiredManager.reviewRequired(task)) {
return requestReview(taskId);
}
try {
taskanaEngine.openConnection();
task = (TaskImpl) this.getTask(taskId);
if (task.getState() == TaskState.COMPLETED) {
return task;

View File

@ -18,6 +18,7 @@ import pro.taskana.spi.routing.api.TaskRoutingProvider;
import pro.taskana.spi.task.api.AfterRequestChangesProvider;
import pro.taskana.spi.task.api.AfterRequestReviewProvider;
import pro.taskana.spi.task.api.CreateTaskPreprocessor;
import pro.taskana.spi.task.api.ReviewRequiredProvider;
import pro.taskana.testapi.WithServiceProvider;
public class ServiceProviderExtractor {
@ -28,6 +29,7 @@ public class ServiceProviderExtractor {
PriorityServiceProvider.class,
TaskRoutingProvider.class,
CreateTaskPreprocessor.class,
ReviewRequiredProvider.class,
AfterRequestReviewProvider.class,
AfterRequestChangesProvider.class);