diff --git a/application-engine/src/main/java/com/netgrif/application/engine/menu/services/DashboardItemServiceImpl.java b/application-engine/src/main/java/com/netgrif/application/engine/menu/services/DashboardItemServiceImpl.java index dc57d15c961..d71b8465c3b 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/menu/services/DashboardItemServiceImpl.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/menu/services/DashboardItemServiceImpl.java @@ -8,9 +8,6 @@ import com.netgrif.application.engine.objects.auth.domain.AbstractUser; import com.netgrif.application.engine.objects.auth.domain.ActorTransformer; import com.netgrif.application.engine.objects.auth.domain.LoggedUser; -import com.netgrif.application.engine.objects.common.ResourceNotFoundException; -import com.netgrif.application.engine.objects.common.ResourceNotFoundExceptionCode; -import com.netgrif.application.engine.objects.petrinet.domain.PetriNet; import com.netgrif.application.engine.objects.petrinet.domain.throwable.TransitionNotExecutableException; import com.netgrif.application.engine.objects.utils.MenuItemUtils; import com.netgrif.application.engine.objects.workflow.domain.Case; @@ -31,9 +28,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; -import java.util.Collections; -import java.util.Locale; -import java.util.Map; +import java.util.*; @Slf4j @Service @@ -74,7 +69,7 @@ public Case getOrCreate(DashboardItemBody body) throws TransitionNotExecutableEx .author(loggedUser) .build()).getCase(); ToDataSetOutcome outcome = body.toDataSet(); - itemCase = setDataWithExecute(itemCase, DashboardItemConstants.TASK_CONFIGURE, outcome.getDataSet()); + itemCase = setDataWithExecute(itemCase, DashboardItemConstants.TASK_CONFIGURE, splitToPhases(outcome.getDataSet())); return itemCase; } @@ -91,7 +86,7 @@ public Case getOrCreate(DashboardItemBody body) throws TransitionNotExecutableEx public Case update(Case itemCase, DashboardItemBody body) throws TransitionNotExecutableException { MenuItemUtils.sanitize(body.getId()); ToDataSetOutcome outcome = body.toDataSet(); - itemCase = setDataWithExecute(itemCase, DashboardItemConstants.TASK_CONFIGURE, outcome.getDataSet()); + itemCase = setDataWithExecute(itemCase, DashboardItemConstants.TASK_CONFIGURE, splitToPhases(outcome.getDataSet())); return itemCase; } @@ -113,20 +108,111 @@ protected Case setData(Case useCase, String transId, Map> dataSet) throws TransitionNotExecutableException { + protected Case setDataWithExecute(Case useCase, String transId, List>> dataSetBatches) throws TransitionNotExecutableException { AbstractUser loggedUser = userService.getLoggedOrSystem(); String taskId = MenuItemUtils.findTaskIdInCase(useCase, transId); + List>> rollbackBatches = buildRollbackBatches(useCase, dataSetBatches); Task task = taskService.findOne(taskId); task = taskService.assignTask(TaskParams.with() .task(task) .user(loggedUser) .build()).getTask(); - task = dataService.setData(task, ImportHelper.populateDataset((Map) dataSet)).getTask(); - return taskService.finishTask(TaskParams.with() - .task(task) - .user(loggedUser) - .build()).getCase(); + try { + for (Map> batch : dataSetBatches) { + task = dataService.setData(task, ImportHelper.populateDataset((Map) batch)).getTask(); + } + return taskService.finishTask(TaskParams.with() + .task(task) + .user(loggedUser) + .build()).getCase(); + } catch (RuntimeException | TransitionNotExecutableException e) { + restoreQuietly(task, rollbackBatches); + cancelQuietly(task, loggedUser); + throw e; + } + } + + /** + * Reads the current values of the fields the batches are going to write, in the same type and batch order. + * Empty for an item that has no id yet: a case that is still being created has nothing to go back to. + */ + private List>> buildRollbackBatches(Case useCase, List>> dataSetBatches) { + Case current = workflowService.findOne(useCase.getStringId()); + Object currentId = current.getDataSet().get(DashboardItemConstants.FIELD_ID) == null ? null + : current.getDataSet().get(DashboardItemConstants.FIELD_ID).getValue(); + if (currentId == null || currentId.toString().isEmpty()) { + return List.of(); + } + List>> rollback = new ArrayList<>(); + for (Map> batch : dataSetBatches) { + Map> restore = new LinkedHashMap<>(); + batch.forEach((fieldId, entry) -> { + Map restoreEntry = new LinkedHashMap<>(); + restoreEntry.put("type", entry.get("type")); + restoreEntry.put("value", current.getDataSet().get(fieldId) == null ? null : current.getDataSet().get(fieldId).getValue()); + restore.put(fieldId, restoreEntry); + }); + rollback.add(restore); + } + return rollback; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void restoreQuietly(Task task, List>> rollbackBatches) { + try { + Task assigned = taskService.findOne(task.getStringId()); + for (Map> batch : rollbackBatches) { + assigned = dataService.setData(assigned, ImportHelper.populateDataset((Map) batch)).getTask(); + } + } catch (RuntimeException e) { + log.warn("Could not restore the data of task {} after a failed dashboard item update", task.getStringId(), e); + } + } + + private void cancelQuietly(Task task, AbstractUser user) { + try { + taskService.cancelTask(TaskParams.with() + .task(taskService.findOne(task.getStringId())) + .user(user) + .build()); + } catch (RuntimeException e) { + log.warn("Could not cancel task {} after a failed dashboard item update", task.getStringId(), e); + } + } + + /** + * Splits the data set into the steps a user takes in the configuration form. Set events of the + * dashboard_item process reset the icon and name whenever the target (is_internal, menu item), the + * inherit flags or external_icon change, so the values of the last step must be written after them. + * The order inside a phase matters too (is_internal clears external_url), hence ordered lists. + */ + private List>> splitToPhases(Map> dataSet) { + List> phases = List.of( + List.of(DashboardItemConstants.FIELD_ID, DashboardItemConstants.FIELD_IS_INTERNAL, + DashboardItemConstants.FIELD_EXTERNAL_URL), + List.of(DashboardItemConstants.FIELD_MENU_ITEM_LIST), + List.of(DashboardItemConstants.FIELD_INHERIT_ICON, DashboardItemConstants.FIELD_INHERIT_NAME), + List.of(DashboardItemConstants.FIELD_EXTERNAL_ICON)); + List>> batches = new ArrayList<>(); + Map> rest = new LinkedHashMap<>(dataSet); + for (List phase : phases) { + Map> batch = new LinkedHashMap<>(); + phase.forEach(fieldId -> { + if (rest.containsKey(fieldId)) { + batch.put(fieldId, rest.remove(fieldId)); + } + }); + batches.add(batch); + } + batches.add(rest); + return batches; } protected Case findCase(String processIdentifier, String query) { diff --git a/application-engine/src/main/resources/petriNets/engine-processes/dashboard_item.xml b/application-engine/src/main/resources/petriNets/engine-processes/dashboard_item.xml index 9b3d54b6d49..dcf62d5cf4f 100644 --- a/application-engine/src/main/resources/petriNets/engine-processes/dashboard_item.xml +++ b/application-engine/src/main/resources/petriNets/engine-processes/dashboard_item.xml @@ -83,8 +83,8 @@ if (item_id.value == "") { return } - def itemWithIdExists = findCaseElastic("processIdentifier:\"dashboard_item\" AND dataSet.item_id.textValue.keyword:\"${item_id.value}\" AND dataSet.is_active.booleanValue:true" as String) - if (itemWithIdExists != null) { + def itemsWithId = findCasesElastic("processIdentifier:\"dashboard_item\" AND dataSet.item_id.textValue.keyword:\"${item_id.value}\" AND dataSet.is_active.booleanValue:true" as String, org.springframework.data.domain.PageRequest.of(0, 100)) + if (itemsWithId.any { it.stringId != useCase.stringId }) { throw new IllegalArgumentException("Dashboard item with given ID already exists.") } @@ -717,8 +717,8 @@ if (item_id.value == null || item_id.value == "") { throw new IllegalArgumentException("Dashboard item ID cannot be empty.") } - def itemWithIdExists = findCaseElastic("processIdentifier:\"dashboard_item\" AND dataSet.item_id.textValue.keyword:\"${item_id.value}\" AND dataSet.is_active.booleanValue:true" as String) - if (itemWithIdExists != null) { + def itemsWithId = findCasesElastic("processIdentifier:\"dashboard_item\" AND dataSet.item_id.textValue.keyword:\"${item_id.value}\" AND dataSet.is_active.booleanValue:true" as String, org.springframework.data.domain.PageRequest.of(0, 100)) + if (itemsWithId.any { it.stringId != useCase.stringId }) { throw new IllegalArgumentException("Dashboard item with given ID already exists.") } if (is_internal.value) { diff --git a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/workflow/domain/menu/dashboard/DashboardItemBody.java b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/workflow/domain/menu/dashboard/DashboardItemBody.java index 23d2e66064c..062762db545 100644 --- a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/workflow/domain/menu/dashboard/DashboardItemBody.java +++ b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/workflow/domain/menu/dashboard/DashboardItemBody.java @@ -36,7 +36,9 @@ public DashboardItemBody(String id, String menuItems, String itemIcon, I18nStrin public ToDataSetOutcome toDataSet() { ToDataSetOutcome outcome = new ToDataSetOutcome(); - outcome.putDataSetEntry(DashboardItemConstants.FIELD_ID, FieldType.TEXT, this.id); + if (this.id != null) { + outcome.putDataSetEntry(DashboardItemConstants.FIELD_ID, FieldType.TEXT, this.id); + } outcome.putDataSetEntry(DashboardItemConstants.FIELD_MENU_ITEM_LIST, FieldType.ENUMERATION_MAP, this.menuItems); outcome.putDataSetEntry(DashboardItemConstants.FIELD_IS_INTERNAL, FieldType.BOOLEAN, this.isInternal); outcome.putDataSetEntry(DashboardItemConstants.FIELD_EXTERNAL_URL, FieldType.TEXT, this.externalUrl);