From 1a53a3cae2e12b0337555801196ad4e8e5f0d260 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 11 May 2026 10:45:02 +0200 Subject: [PATCH 01/20] Add migration tests and migration helper utility Added `MigrationTest` class to validate case migration processes and introduced the `MigrationHelper` utility to streamline case and task migrations. These changes improve testing coverage and simplify migration workflows through reusable and efficient methods. --- .../engine/migration/MigrationHelper.groovy | 980 ++++++++++++++++++ .../MigrationConfigurationProperties.groovy | 27 + .../helpers/CaseMigrationHelper.groovy | 181 ++++ .../engine/migration/MigrationTest.groovy | 126 +++ src/test/resources/nae_2432_v1.xml | 257 +++++ src/test/resources/nae_2432_v2.xml | 388 +++++++ 6 files changed, 1959 insertions(+) create mode 100644 src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy create mode 100644 src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy create mode 100644 src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy create mode 100644 src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy create mode 100644 src/test/resources/nae_2432_v1.xml create mode 100644 src/test/resources/nae_2432_v2.xml diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy new file mode 100644 index 00000000000..3b9c3c3e0cd --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy @@ -0,0 +1,980 @@ +package com.netgrif.application.engine.migration + +import com.netgrif.application.engine.auth.service.interfaces.IUserService +import com.netgrif.application.engine.elastic.service.interfaces.* +import com.netgrif.application.engine.importer.service.Importer +import com.netgrif.application.engine.petrinet.domain.I18nString +import com.netgrif.application.engine.petrinet.domain.PetriNet +import com.netgrif.application.engine.petrinet.domain.Transition +import com.netgrif.application.engine.petrinet.domain.VersionType +import com.netgrif.application.engine.petrinet.domain.dataset.* +import com.netgrif.application.engine.petrinet.domain.events.Event +import com.netgrif.application.engine.petrinet.domain.events.EventType +import com.netgrif.application.engine.petrinet.domain.repositories.PetriNetRepository +import com.netgrif.application.engine.petrinet.domain.roles.ProcessRole +import com.netgrif.application.engine.petrinet.domain.roles.ProcessRoleRepository +import com.netgrif.application.engine.petrinet.service.PetriNetService +import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService +import com.netgrif.application.engine.workflow.domain.* +import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository +import com.netgrif.application.engine.workflow.domain.repositories.TaskRepository +import com.netgrif.application.engine.workflow.service.interfaces.ITaskService +import com.querydsl.core.types.Predicate +import groovy.util.logging.Slf4j +import org.apache.tomcat.util.http.fileupload.IOUtils +import org.bson.types.ObjectId +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.core.io.ClassPathResource +import org.springframework.core.io.Resource +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.stereotype.Component + +import javax.inject.Provider +import java.text.Collator +import java.time.LocalDateTime +import java.util.stream.Collectors + +@Slf4j +@Component +class MigrationHelper { + + @Autowired + private CaseRepository caseRepository + + @Autowired + private TaskRepository taskRepository + + @Autowired + private PetriNetService service + + @Autowired + private Provider importerProvider + + @Autowired + private ProcessRoleRepository roleRepository + + @Autowired + private PetriNetRepository netRepository + + @Autowired + private ITaskService taskService + + @Autowired + private IElasticCaseService elasticCaseService + + @Autowired + private IElasticCaseMappingService caseMappingService + + @Autowired + private IElasticTaskService elasticTaskService + + @Autowired + private IElasticTaskMappingService elasticTaskMappingService + + @Autowired + private IUserService userService + + @Autowired + private IElasticIndexService elasticIndexService + + @Autowired + private MongoTemplate mongoTemplate + + @Autowired + private IPetriNetService petriNetService + + private Importer getImporter() { + return importerProvider.get() + } + + /** + * Updates all cases filtered by filter Predicate. Update closure is called on each filtered case. + * @param update Instance of Closure, which should contain code that will be executed for every Case matched by filter + * @param filter Instance of Predicate, to filter which cases should be updated + */ + void updateCases(Closure update, Predicate filter) { + log.info("Updating cases with filter ${filter.toString()} and update ${update.toString()}") + iterateCases(update, { Page cases -> caseRepository.saveAll(cases) }, filter) + } + + /** + * Iterates all cases filtered by filter Predicate. Update closure is called on each filtered case. PageProcessed closure is called after each page iteration. + * @param update Instance of Closure, which should contain code that will be executed for every Case matched by filter (changes made to Case will not be saved automatically, for that use updateCases method) + * @param sleepFor Optional attribute to set sleep time (in milliseconds) to sleep for after each iterated page. Default 0ms + * @param filter Instance of Predicate, to filter which cases should be iterated + */ + void iterateCases(Closure update, Closure pageProcessed = {}, long sleepFor = 0, Predicate filter) { + long caseCount = caseRepository.count(filter) + long numOfPages = ((caseCount / 100.0) + 1) as long + log.info("Processing cases with filter ${filter.toString()}: $numOfPages pages") + numOfPages.times { page -> + log.info("Page $page / $numOfPages") + + Page cases = caseRepository.findAll(filter, PageRequest.of(page, 100)) + + cases.each { + log.debug("Processing case with id ${it.stringId}") + log.trace("Processing case ${it.toString()}") + update(it) + } + pageProcessed(cases) + if (sleepFor != 0) { + log.debug("Pausing migration for ${sleepFor} milliseconds") + sleep(sleepFor) + } + } + } + + /** + * Updates all cases of a given process. + * @param update Instance of Closure, which should contain code that will be executed for every Case matched by filter + * @param processIdentifier identifier of PetriNet, to filter which cases should be updated + * @param pageSize Optional attribute to set page size. Default page size 100.0 + */ + void updateCasesCursor(Closure update, String processIdentifier, double pageSize = 100.0) { + long caseCount = caseRepository.count(QCase.case$.processIdentifier.eq(processIdentifier)) + long numOfPages = ((caseCount / pageSize) + 1) as long + log.info("Migrating process $processIdentifier") + log.info("Page size: $pageSize") + log.info("Processing cases: $numOfPages pages") + ObjectId lastId = null + if (caseCount > 0) { + for (int p = 0; p < numOfPages; p++) { + try { + log.info("Page " + (p + 1) + " / $numOfPages") + + Query query = new Query() + if (lastId == null) { + query.skip(0) + } else { + query.addCriteria(Criteria.where("_id").gt(lastId)) + } + query.addCriteria(Criteria.where("processIdentifier").is(processIdentifier)) + query.limit(pageSize as Integer) + + List cases = mongoTemplate.find(query, Case.class) + cases.each { update(it) } + cases = caseRepository.saveAll(cases) + + lastId = cases.get(cases.size() - 1).get_id() + } catch (Exception e) { + log.error("Failed to iterate page " + (p + 1), e.getMessage()) + break + } + } + } + } + + /** + * Update all cases. + * @param update Instance of Closure, which should contain code that will be executed for every Case + * @param pageSize Optional attribute to set page size. Default page size 100.0 + */ + void updateAllCasesCursor(Closure update, double pageSize = 100.0) { + long caseCount = caseRepository.count() + long numOfPages = ((caseCount / pageSize) + 1) as long + log.info("Page size: $pageSize") + log.info("Processing cases: $numOfPages pages") + ObjectId lastId = null + if (caseCount > 0) { + for (int p = 0; p < numOfPages; p++) { + try { + log.info("Page " + (p + 1) + " / $numOfPages") + + Query query = new Query() + if (lastId == null) { + query.skip(0) + } else { + query.addCriteria(Criteria.where("_id").gt(lastId)) + } + query.limit(pageSize as Integer) + + List cases = mongoTemplate.find(query, Case.class) + cases.each { update(it) } + cases = caseRepository.saveAll(cases) + + lastId = cases.get(cases.size() - 1).get_id() + } catch (ArrayIndexOutOfBoundsException e) { + log.error("Failed to iterate page " + (p + 1)) + break + } + } + } + } + + /** + * Updates all tasks filtered by filter Predicate. Update closure is called on each filtered task. + * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter + * @param filter Instance of Predicate, to filter which tasks should be updated + */ + void updateTasks(Closure update, Predicate filter) { + log.info("Updating tasks with filter ${filter.toString()} and update ${update.toString()}") + iterateTasks(update, { Page tasks -> taskRepository.saveAll(tasks) }, filter) + } + + /** + * Iterates all tasks filtered by filter Predicate. Update closure is called on each filtered task. PageProcessed closure is called after each page iteration. + * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter (changes made to Task will not be saved automatically, for that use updateCases method) + * @param sleepFor Optional attribute to set sleep time (in milliseconds) to sleep for after each iterated page. Default 0ms + * @param filter Instance of Predicate, to filter which tasks should be iterated + */ + void iterateTasks(Closure update, Closure pageProcessed = {}, long sleepFor = 0, Predicate filter) { + long taskCount = taskRepository.count(filter) + long numOfPages = ((taskCount / 100.0) + 1) as long + log.info("Processing tasks with filter ${filter.toString()}: $numOfPages pages") + numOfPages.times { page -> + log.info("Page $page / $numOfPages") + + Page tasks = taskRepository.findAll(filter, PageRequest.of(page, 100)) + + tasks.each { update(it) } + pageProcessed(tasks) + if (sleepFor != 0) { + sleep(sleepFor) + } + } + } + + /** + * Updates all tasks of a given process. + * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter + * @param processIdentifier identifier of PetriNet, to filter which tasks should be updated + * @param pageSize Optional attribute to set page size. Default page size 100.0 + */ + void updateTasksCursor(Closure update, String processIdentifier, double pageSize = 100.0) { + String processId = petriNetService.getNewestVersionByIdentifier(processIdentifier).stringId + long taskCount = taskRepository.count(QTask.task.processId.eq(processId)) + long numOfPages = ((taskCount / pageSize) + 1) as long + log.info("Migrating process $processIdentifier") + log.info("Page size: $pageSize") + log.info("Processing tasks: $numOfPages pages") + ObjectId lastId = null + if (taskCount > 0) { + for (int p = 0; p < numOfPages; p++) { + try { + log.info("Page " + (p + 1) + " / $numOfPages") + + Query query = new Query() + if (lastId == null) { + query.skip(0) + } else { + query.addCriteria(Criteria.where("_id").gt(lastId)) + } + query.addCriteria(Criteria.where("processId").is(processId)) + query.limit(pageSize as Integer) + + List tasks = mongoTemplate.find(query, Task.class) + tasks.each { update(it) } + tasks = taskRepository.saveAll(tasks) + + lastId = tasks.get(tasks.size() - 1).objectId + } catch (ArrayIndexOutOfBoundsException e) { + log.error("Failed to iterate page " + (p + 1)) + break + } + } + } + } + + /** + * Updates specific tasks of a given process. + * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter + * @param processIdentifier identifier of PetriNet, to filter which tasks should be updated + * @param transitionIds List of transition IDs to limit filter to specific transitions of given processIdentifier + * @param pageSize Optional attribute to set page size. Default page size 100.0 + */ + void updateSpecificTasksCursor(Closure update, String processIdentifier, List transitionIds, double pageSize = 100.0) { + String processId = petriNetService.getNewestVersionByIdentifier(processIdentifier).stringId + long taskCount = taskRepository.count(QTask.task.processId.eq(processId) & QTask.task.transitionId.in(transitionIds)) + long numOfPages = ((taskCount / pageSize) + 1) as long + log.info("Migrating process $processIdentifier transitions ${transitionIds.toString()}") + log.info("Page size: $pageSize") + log.info("Processing tasks: $numOfPages pages") + ObjectId lastId = null + if (taskCount > 0) { + for (int p = 0; p < numOfPages; p++) { + try { + log.info("Page " + (p + 1) + " / $numOfPages") + + Query query = new Query() + if (lastId == null) { + query.skip(0) + } else { + query.addCriteria(Criteria.where("_id").gt(lastId)) + } + query.addCriteria(Criteria.where("processId").is(processId)) + query.addCriteria(Criteria.where("transitionId").in(transitionIds)) + query.limit(pageSize as Integer) + + List tasks = mongoTemplate.find(query, Task.class) + tasks.each { update(it) } + tasks = taskRepository.saveAll(tasks) + + lastId = tasks.get(tasks.size() - 1).objectId + } catch (ArrayIndexOutOfBoundsException e) { + log.error("Failed to iterate page " + (p + 1)) + break + } + } + } + } + + /** + * Update all tasks. + * @param update Instance of Closure, which should contain code that will be executed for every Task + * @param pageSize Optional attribute to set page size. Default page size 100.0 + */ + void updateAllTasksCursor(Closure update, double pageSize = 100.0) { + long taskCount = taskRepository.count() + long numOfPages = ((taskCount / pageSize) + 1) as long + log.info("Page size: $pageSize") + log.info("Processing tasks: $numOfPages pages") + ObjectId lastId = null + if (taskCount > 0) { + for (int p = 0; p < numOfPages; p++) { + try { + log.info("Page " + (p + 1) + " / $numOfPages") + + Query query = new Query() + if (lastId == null) { + query.skip(0) + } else { + query.addCriteria(Criteria.where("_id").gt(lastId)) + } + query.limit(pageSize as Integer) + + List tasks = mongoTemplate.find(query, Task.class) + tasks.each { update(it) } + tasks = taskRepository.saveAll(tasks) + + lastId = tasks.get(tasks.size() - 1).objectId + } catch (ArrayIndexOutOfBoundsException e) { + log.error("Failed to iterate page " + (p + 1)) + break + } + } + } + } + + /** + * Updates existing Petri Net model with new values. New process roles are ignored! New roles in existing user type fields will be ignored! + * @param identifier Identifier of Petri Net model that is being updated + * @param resource Resource object with new version of Petri Net model + */ + void updateNetIgnoreRoles(String identifier, Resource resource, List> customUpdates = null) { + PetriNet reimported = service.importPetriNet(resource.inputStream, VersionType.MAJOR, userService.system.transformToLoggedUser()).getNet() + updateNetIgnoreRoles(service.getNewestVersionByIdentifier(identifier), reimported, customUpdates) + } + + /** + * Updates existing Petri Net model with new values. New process roles are ignored! New roles in existing user type fields will be ignored! + * @param identifier Identifier of Petri Net model that is being updated + * @param fileName File name of new version of Petri Net model + */ + void updateNetIgnoreRoles(String identifier, String fileName, List> customUpdates = null) { + PetriNet currentNet = service.getNewestVersionByIdentifier(identifier) + InputStream inputStream = new ClassPathResource("petriNets/$fileName" as String).inputStream + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + IOUtils.copy(inputStream, outputStream) + PetriNet reimported = getImporter().importPetriNet(new ByteArrayInputStream(outputStream.toByteArray())).get() + updateNetIgnoreRoles(currentNet, reimported, customUpdates) + } + + /** + * Updates existing Petri Net model with new values. New process roles are ignored! New roles in existing user type fields will be ignored! + * @param currentNet Current Petri Net object that will be updated + * @param reimported New version of Petri Net object, its values will be applied to currentNet + */ + void updateNetIgnoreRoles(PetriNet currentNet, PetriNet reimported, List> customUpdates) { + if (!currentNet) { + log.warn("Net $reimported.identifier does not exist") + return + } + Map oldProcessRoles = currentNet.roles + Map newProcessRoles = reimported.roles + + reimported = replaceUserFieldRoleReferences(currentNet, reimported) + + ProcessRole defaultRole = roleRepository.findAllByName_DefaultValue(ProcessRole.DEFAULT_ROLE).first() + ProcessRole anonymousRole = roleRepository.findAllByName_DefaultValue(ProcessRole.ANONYMOUS_ROLE).first() + + currentNet.places = reimported.places + currentNet.transitions = reimported.transitions + currentNet.arcs = reimported.arcs + currentNet.dataSet = reimported.clone().dataSet + currentNet.transactions = reimported.transactions + currentNet.importId = reimported.importId + currentNet.caseEvents = reimported.caseEvents + currentNet.processEvents = reimported.processEvents + currentNet.negativeViewRoles = reimported.negativeViewRoles + currentNet.userRefs = reimported.userRefs + currentNet.functions = reimported.functions + + def newPermissions = [:] + reimported.permissions.each { id, permissions -> + def newRole = newProcessRoles[id] + + if (!newRole && (defaultRole.stringId == id || anonymousRole.stringId == id)) { + log.info("Default role $id on process $currentNet.identifier detected, skipping") + newPermissions[id] = permissions + + } else { + def oldRole = oldProcessRoles.values().find { + it.importId == newRole.importId + } + + if (!oldRole) { + log.warn("Old role does not exist for role $newRole.importId") + return + } + newPermissions[oldRole.stringId] = permissions + } + } + currentNet.permissions = newPermissions as Map> + + currentNet.transitions.each { id, t -> + Map> oldRoles = new HashMap<>() + t.roles.each { roleMongoId, permissions -> + def newRole = newProcessRoles[roleMongoId] + + if (!newRole && (defaultRole.stringId == roleMongoId || anonymousRole.stringId == roleMongoId)) { + log.info("Default role $roleMongoId on transition ${t.importId} detected, skipping") + oldRoles[roleMongoId] = permissions + + } else { + def oldRole = oldProcessRoles.values().find { + it.importId == newRole.importId + } + + if (!oldRole) { + log.warn("Old role does not exist for role $newRole.importId") + return + } + oldRoles[oldRole.stringId] = permissions + } + } + t.roles = oldRoles + } + + resolveDataOrder(currentNet) + + customUpdates && customUpdates.each { Closure customUpdate -> + currentNet = customUpdate(currentNet, reimported) + } + + service.save(currentNet) + log.info("Migrated $currentNet.identifier") + } + + /** + * Replaces role permissions on transition with provided map e.g. ["roleId": ["perform": true]] + * @param net Instance of Petri Net in which role on transition will be updated + * @param transitionId Transition ID of updated transition + * @param role ProcessRole that will be updated on transition + * @param permissions New role permissions on transition + */ + void updateTransitionRoles(PetriNet net, String transitionId, ProcessRole role, Map permissions) { + Transition trans = net.transitions.values().find { it.importId == transitionId } + trans.roles[role.stringId] = permissions + } + + /** + * Replaces role permissions on transition with provided map e.g. ["roleId": ["perform": true]] + * @param net Instance of Petri Net in which role on transition will be updated + * @param transitionId Transition ID of updated transition + * @param roleImportId ID of a role that will be updated on transition + * @param permissions New role permissions on transition + */ + void updateTransitionRoles(PetriNet net, String transitionId, String roleImportId, Map permissions) { + ProcessRole role = net.roles.values().find { it.importId == roleImportId } + updateTransitionRoles(net, transitionId, role, permissions) + } + + /** + * Replaces role permissions on transition with provided map e.g. ["roleId": ["perform": true]] + * @param transitionId Transition ID of updated transition + * @param roleImportId ID of a role that will be updated on transition + * @param permissions New role permissions on transition + */ + Closure updateTransitionRolesClosure(String transitionId, String roleImportId, Map permissions) { + return { PetriNet petriNet, PetriNet reimported -> + updateTransitionRoles(petriNet, transitionId, roleImportId, permissions) + return petriNet + } + } + + /** + * Updates data set of existing Petri Net model with new values. + * @param identifier Identifier of Petri Net model that is being updated + * @param fileName File name of new version of Petri Net model + */ + void updateDataSet(String identifier, String fileName, Closure customUpdate = null) { + PetriNet existing = service.getNewestVersionByIdentifier(identifier) + PetriNet reimported = getImporter().importPetriNet(new File("src/main/resources/petriNets/" + fileName)).get() + + reimported = replaceUserFieldRoleReferences(existing, reimported) + + existing.dataSet = reimported.dataSet + + if (customUpdate) { + existing = customUpdate(existing, reimported) + } + + service.save(existing) + log.info("Migrated $identifier") + } + + /** + * Updates roles of USER fields in existing Petri Net model, WARNING: new roles referenced in USER fields will be ignored! They need to be migrated manually + * @param originalNet Current Petri Net object that will be updated + * @param reimportedNet New version of Petri Net object, its values will be applied to currentNet + */ + private PetriNet replaceUserFieldRoleReferences(PetriNet originalNet, PetriNet reimportedNet) { + Map originalNetRoles = [:] // importId: processRole + originalNet.roles.forEach { name, role -> + originalNetRoles.put(role.importId, role) + } + + reimportedNet.dataSet.entrySet().stream().filter { + it.value.type == FieldType.USER + + }.forEach { entry -> + UserField field = (reimportedNet.dataSet[entry.key] as UserField) + field.roles = field.roles.collect { roleId -> + Optional roleOpt = Optional.ofNullable(reimportedNet.roles[roleId]) + if (roleOpt.isPresent()) { + ProcessRole oldRole = originalNetRoles[roleOpt.get().importId] + + if (!oldRole) { + log.warn("Process role in process ${originalNet.identifier} ${originalNet.stringId} with import id ${roleOpt.get().importId} not found!") + return null + + } else { + return oldRole.stringId + } + + } else { + log.warn("Role not found! ${roleId}") + return null + } + }.stream().filter { Objects.nonNull(it) }.collect() + + } + + return reimportedNet + } + + /** + * Create new role in existing Petri Net model. + * @param identifier Identifier of Petri Net model in which the Process Role will be created + * @param id ID of the new Process Role + * @param title Title of the new Process Role + */ + def createRoleInNet(String identifier, String id, String title, Map events = [:]) { + return createRoleInNet(identifier, id, new I18nString(title), events) + } + + /** + * Create new role in existing Petri Net model. + * @param identifier Identifier of Petri Net model in which the Process Role will be created + * @param id ID of the new Process Role + * @param title Title of the new Process Role + */ + def createRoleInNet(String identifier, String id, I18nString title, Map events = [:]) { + PetriNet net = service.getNewestVersionByIdentifier(identifier) + + ProcessRole role = new ProcessRole() + role.setImportId(id) + role.setName(title) + role.setEvents(events) + + role = roleRepository.save(role) + net.addRole(role) + netRepository.save(net) + + return role + } + + /** + * Creates new global role + * @param id ID of the new Process Role + * @param title Title of the new Process Role + */ + def createGlobalRole(String id, String title, Map events = [:]) { + return createGlobalRole(id, new I18nString(title), events) + } + + /** + * Creates new global role + * @param id ID of the new Process Role + * @param title Title of the new Process Role + */ + def createGlobalRole(String id, I18nString title, Map events = [:]) { + ProcessRole role = new ProcessRole() + + if (!id.startsWith("global_")) { + role.setImportId("global_" + id) + } else { + role.setImportId(id) + } + role.setName(title) + role.setEvents(events) + role.setGlobal(true) + + role = roleRepository.save(role) + + return role + } + + /** + * Replaces events in roles from existing with events from roles from reimported + */ + Closure updateRoleEvents = { PetriNet existing, PetriNet reimported -> + List newRoles = reimported.roles.values() as List + List oldRoles = existing.roles.values() as List + + newRoles.each { newRole -> + ProcessRole role = oldRoles.find { it.importId == newRole.importId } + role.events = newRole.events + roleRepository.save(role) + } + + return existing + } + + /** + * Reloads tasks of provided case via TaskService, + * handles useCase.petriNet internally + * @param useCase Instance of Case for which tasks will be reloaded + * @param net Instance of Petri Net, it needs to match processIdentifier of useCase + */ + void reloadTasks(Case useCase, PetriNet net) { + setPetriNet(useCase, net) + taskService.reloadTasks(useCase) + } + + /** + * Indexes provided case in elasticsearch + * handles useCase.petriNet internally + * @param useCase Instance of Case that will be indexed into elasticsearch index + */ + void elasticIndex(Case useCase) { + try { + setPetriNet(useCase, service.getNewestVersionByIdentifier(useCase.processIdentifier)) + assert useCase.petriNet + elasticCaseService.indexNow(caseMappingService.transform(useCase)) + } catch (Exception ex) { + if (useCase.lastModified == null) { + log.error("Creating new lastModified date for $useCase.stringId") + useCase.lastModified = LocalDateTime.now() + elasticCaseService.indexNow(caseMappingService.transform(useCase)) + } else { + log.error("Failed to index $useCase.stringId", ex) + } + } + } + + /** + * Indexes provided task in elasticsearch + * @param task Instance of Task that will be indexed into elasticsearch index + */ + void elasticTaskIndex(Task task) { + try { + elasticTaskService.indexNow(elasticTaskMappingService.transform(task)) + } catch (Exception e) { + log.error("Failed to index $task.stringId", e) + } + } + + /** + * Adds role with permissions to existing tasks of net + * @param role ProcessRole that will be added to transitions + * @param net Instance of Petri Net of updated transitions + * @param transitionIds List of transition IDs the role will be added to + * @param permissions Map of permissions for the role + */ + void addRoleToExistingTasks(ProcessRole role, PetriNet net, List transitionIds, Map permissions) { + updateTasks({ Task task -> + log.info("Add role '${role.getName()}' with roleId=${role.getImportId()} to transitionId=${task.getTransitionId()} in task ${task.stringId}") + task.addRole(role.getStringId(), permissions) + }, QTask.task.transitionId.in(transitionIds) & QTask.task.processId.eq(net.getStringId())) + } + + /** + * Sets petriNet object in case instance + * @param useCase Instance of Case + * @param net Instance of Petri Net, it needs to match processIdentifier of useCase + */ + void setPetriNet(Case useCase, PetriNet net) { + PetriNet model = net.clone() + model.initializeTokens(useCase.getActivePlaces()) + model.initializeArcs(useCase.getDataSet()) + useCase.setPetriNet(model) + } + + /** + * Delete given data fields from useCase + * @param useCase Instance of Case + * @param toDelete List of field IDs that will be deleted from useCase + */ + void deleteDataFields(Case useCase, List toDelete) { + toDelete.each { dataFieldID -> + useCase.dataSet.remove(dataFieldID) + } + } + + /** + * Changes value of given data fields from number to text + * @param useCase Instance of Case + * @param toChange List of field IDs for value change + */ + void changeDataFieldsValueFromNumberToText(Case useCase, List toChange) { + toChange.each { dataFieldID -> + if (useCase.dataSet[dataFieldID].value && (useCase.dataSet[dataFieldID].value != null || useCase.dataSet[dataFieldID].value != "")) { + double value = useCase.dataSet[dataFieldID].value as double + useCase.dataSet[dataFieldID].value = value as String + } + } + } + + /** + * Changes value of given data fields from text to number + * @param useCase Instance of Case + * @param toChange List of field IDs for value change + */ + void changeDataFieldsValueFromTextToNumber(Case useCase, List toChange) { + toChange.each { dataFieldID -> + if (useCase.dataSet[dataFieldID].value && useCase.dataSet[dataFieldID].value != "") { + try { + useCase.dataSet[dataFieldID].value = useCase.dataSet[dataFieldID].value as double + } catch (Exception e) { + useCase.dataSet[dataFieldID].value = null + log.error("[${useCase.stringId}] could not convert value ${useCase.dataSet[dataFieldID].value} in field ${dataFieldID}", e) + } + } + } + } + + /** + * Adds new data fields with their init value into useCase + * @param useCase Instance of Case + * @param toAdd Map + */ + void addTextDataFields(Case useCase, Map toAdd) { + toAdd.each { dataFieldID, value -> + useCase.dataSet[dataFieldID] = new DataField(value) + } + } + + /** + * Changes value of given data fields from enumeration to multichoice + * @param useCase Instance of Case + * @param toChange List of field IDs for value change + */ + void changeDataFieldsValueFromEnumerationToMultichoice(Case useCase, List toChange) { + toChange.each { dataFieldID -> + if (useCase.dataSet[dataFieldID].value && useCase.dataSet[dataFieldID].value != null) { + def value + if (useCase.dataSet[dataFieldID].value instanceof I18nString) { + value = useCase.dataSet[dataFieldID].value as I18nString + } else { + value = new I18nString(useCase.dataSet[dataFieldID].value as String) + } + + def newSet = new HashSet() + newSet.add(value) + useCase.dataSet[dataFieldID].value = newSet + } + } + } + + /** + * Adds new choices into enumeration or multichoice field + * @param useCase Instance of Case + * @param toAdd Map + */ + void addChoices(Case useCase, Map> toAdd) { + toAdd.each { dataFieldID, newChoices -> + if (useCase.dataSet[dataFieldID].choices == null) { + useCase.dataSet[dataFieldID].setChoices(new HashSet()) + } + + newChoices.each { + useCase.dataSet[dataFieldID].choices.add(new I18nString(it)) + } + } + } + + /** + * Removes choices from enumeration or multichoice field + * @param useCase Instance of Case + * @param toAdd Map + */ + void removeChoices(Case useCase, Map> toRemove) { + toRemove.each { dataFieldID, choicesToRemove -> + if (useCase.dataSet[dataFieldID].value != null) { + (useCase.dataSet[dataFieldID].value as Set).removeAll(choicesToRemove) + } + + if (useCase.dataSet[dataFieldID].choices != null) { + useCase.dataSet[dataFieldID].choices.removeAll(choicesToRemove) + } + } + } + + /** + * Changes value from FileFieldValue to FileListFieldValue + * @param useCase Instance of Case + * @param fieldId Field ID for value change + */ + private void changeFileFieldToFileList(Case useCase, String fieldId) { + FileListFieldValue fileListFieldValue = new FileListFieldValue() + fileListFieldValue.namesPaths.add(useCase.dataSet[fieldId].value as FileFieldValue) + useCase.dataSet[fieldId].value = fileListFieldValue + } + + /** + * Helper method used in updateNetIgnoreRoles method, it sorts PetriNet dataSet alphabetically + * @param petriNet Instance of Petri Net + */ + void resolveDataOrder(PetriNet petriNet) { + Collator skCollator = Collator.getInstance(new Locale("sk", "SK")) + List fields = new LinkedList<>(petriNet.getDataSet().values()) + fields = fields.stream().sorted({ f1, f2 -> + int comparedTypes = f2.type.name <=> f1.type.name + if (comparedTypes != 0) return comparedTypes + return skCollator.compare((f1.name?.defaultValue ?: f1.stringId), (f2.name?.defaultValue ?: f2.stringId)) + }).collect(Collectors.toList()) + petriNet.dataSet = fields.collectEntries { [(it.getStringId()): (it)] } as Map + } + + /** + * Update dataField and dataRef components of given case + * @param useCase Instance of Case + * @param net Instance of Petri Net, it needs to match processIdentifier of useCase + */ + void updateCaseComponents(Case useCase, PetriNet net) { + Map components = createComponentsMap(net) + Map> dataRefComponents = createDataRefComponentsMap(net) + + useCase.dataSet.each {dataField -> + if (components[dataField.key]) { + useCase.dataSet[dataField.key].component = components[dataField.key] + } + if (dataRefComponents[dataField.key]) { + useCase.dataSet[dataField.key].dataRefComponents = dataRefComponents[dataField.key] + } + } + } + + /** + * Method that collects all dataRef components of given PetriNet. Should be used in updateCases method, when a new dataRef component is added into PetriNet. + * @param net Instance of PetriNet + */ + Map> createDataRefComponentsMap(PetriNet net) { + Map> componentsMap = [:] + net.transitions.each {transition -> + String transId = transition.key + transition.value.dataSet.each {dataField -> + String fieldId = dataField.key + if (dataField.value.component) { + if (!componentsMap[fieldId]) { + componentsMap.put(fieldId, [(transId) : dataField.value.component]) + } else { + Map existingMap = componentsMap[fieldId] + existingMap.put(transId, dataField.value.component) + componentsMap.put(fieldId, existingMap) + } + } + } + } + return componentsMap + } + + /** + * Method that collects all dataField components of given PetriNet. Should be used in updateCases method, when a new dataField component is added into PetriNet. + * @param net Instance of PetriNet + */ + Map createComponentsMap(PetriNet net) { + Map componentsMap = [:] + net.dataSet.each {dataField -> + if (dataField.value.component) { + componentsMap.put(dataField.key, dataField.value.component) + } + } + return componentsMap + } + + /** + * Updates case permissions from PetriNet + * @param useCase Instance of Case + * @param net Instance of Petri Net, it needs to match processIdentifier of useCase + */ + void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false) { + useCase.permissions = net.getPermissions().entrySet().stream() + .filter(role -> role.getValue().containsKey("delete") || role.getValue().containsKey("view")) + .map(role -> { + Map permissionMap = new HashMap<>() + if (role.getValue().containsKey("delete")) + permissionMap.put("delete", role.getValue().get("delete")) + if (role.getValue().containsKey("view")) { + permissionMap.put("view", role.getValue().get("view")) + } + return new AbstractMap.SimpleEntry<>(role.getKey(), permissionMap) + }) + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)) + useCase.resolveViewRoles() + useCase.setEnabledRoles(net.getRoles().keySet()) + if (updateTasks) { + useCase.tasks.each { taskPair -> + updateTaskPermissions(useCase, taskPair, net) + } + } + } + + /** + * Updates permissions on existing tasks filtered by relevantTransitionIds + * @param useCase Instance of Case + * @param net Instance of Petri Net, it needs to match processIdentifier of useCase + * @param relevantTransitionIds List of transition IDs for permissions update + */ + void updateTasksPermissions(Case useCase, PetriNet net, List relevantTransitionIds) { + useCase.tasks.findAll { it.transition in relevantTransitionIds }.each { taskPair -> + updateTaskPermissions(useCase, taskPair, net) + } + } + + /** + * Updates permissions on existing task + * @param useCase Instance of Case + * @param taskPair TaskPair object of updated Task + * @param net Instance of Petri Net, it needs to match processIdentifier of useCase + */ + void updateTaskPermissions(Case useCase, TaskPair taskPair, PetriNet net) { + try { + Transition newTransition = net.getTransition(taskPair.transition) + Task oldTask = taskService.findOne(taskPair.task) + oldTask.setProcessId(net.stringId) + oldTask.getRoles().clear() + oldTask.setRoles(newTransition.roles) + oldTask.setNegativeViewRoles(newTransition.negativeViewRoles) + oldTask.resolveViewRoles() + taskService.save(oldTask) + } catch (Exception e) { + log.error("Failed to update task permissions $useCase.stringId $taskPair.transition", e) + } + } + + /** + * Changes PetriNet reference in useCase + * @param useCase Instance of Case + * @param newNet Instance of Petri Net, it needs to match processIdentifier of useCase + */ + void migratePetriNet(Case useCase, PetriNet newNet) { + useCase.setPetriNetObjectId(newNet.objectId) + } +} \ No newline at end of file diff --git a/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy b/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy new file mode 100644 index 00000000000..15f4fe3a4b3 --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy @@ -0,0 +1,27 @@ +package com.netgrif.application.engine.migration.config.properties + +import lombok.Data +import lombok.Getter +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.stereotype.Component + +@Component +@ConfigurationProperties(prefix = "netgrif.migration") +class MigrationConfigurationProperties { + + private CaseMigrationProperties cases = new CaseMigrationProperties() + + @Data + static class CaseMigrationProperties { + + private int pageSize = 100 + + int getPageSize() { + return pageSize + } + } + + CaseMigrationProperties getCases() { + return cases + } +} diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy new file mode 100644 index 00000000000..e350e21775d --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -0,0 +1,181 @@ +package com.netgrif.application.engine.migration.helpers + +import com.mongodb.BulkWriteError +import com.mongodb.BulkWriteException +import com.mongodb.bulk.BulkWriteResult +import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties +import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.CaseMigrationProperties +import com.netgrif.application.engine.workflow.domain.Case +import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository +import com.querydsl.core.types.Predicate +import groovy.util.logging.Slf4j +import lombok.RequiredArgsConstructor +import org.bson.types.ObjectId +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort +import org.springframework.data.mongodb.core.BulkOperations +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.util.CloseableIterator +import org.springframework.stereotype.Component + +@Slf4j +@Component +@RequiredArgsConstructor +class CaseMigrationHelper { + + public static final String CASE_COLLECTION_NAME = "case" + + private MongoTemplate mongoTemplate + + private CaseRepository caseRepository + + private CaseMigrationProperties caseMigrationProperties + + @Autowired + void setMongoTemplate(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate + } + + @Autowired + void setCaseRepository(CaseRepository caseRepository) { + this.caseRepository = caseRepository + } + + @Autowired + void setCaseMigrationProperties(MigrationConfigurationProperties migrationConfigurationProperties) { + this.caseMigrationProperties = migrationConfigurationProperties.cases + } +/** + * Updates all cases filtered by filter Predicate. Update closure is called on each filtered case. + * @param update Instance of Closure, which should contain code that will be executed for every Case matched by filter + * @param filter Instance of Predicate, to filter which cases should be updated + */ + void updateCases(Closure update, Predicate filter) { + log.info("Updating cases with filter ${filter.toString()} and update ${update.toString()}") + iterateCases(update, { Page cases -> caseRepository.saveAll(cases) }, filter) + } + + /** + * Iterates all cases filtered by filter Predicate. Update closure is called on each filtered case. PageProcessed closure is called after each page iteration. + * @param update Instance of Closure, which should contain code that will be executed for every Case matched by filter (changes made to Case will not be saved automatically, for that use updateCases method) + * @param sleepFor Optional attribute to set sleep time (in milliseconds) to sleep for after each iterated page. Default 0ms + * @param filter Instance of Predicate, to filter which cases should be iterated + */ + void iterateCases(Closure update, Closure pageProcessed = {}, long sleepFor = 0, Predicate filter) { + long caseCount = caseRepository.count(filter) + long numOfPages = ((caseCount / caseMigrationProperties.pageSize) + 1) as long + log.info("Processing cases with filter ${filter.toString()}: $numOfPages pages") + numOfPages.times { page -> + log.info("Page $page / $numOfPages") + + Page cases = caseRepository.findAll(filter, PageRequest.of(page, caseMigrationProperties.pageSize)) + + cases.each { + log.debug("Processing case with id ${it.stringId}") + log.trace("Processing case ${it.toString()}") + update(it) + } + pageProcessed(cases) + if (sleepFor != 0) { + log.debug("Pausing migration for ${sleepFor} milliseconds") + sleep(sleepFor) + } + } + } + + /** + * Updates all cases of a given process. + * @param update Instance of Closure, which should contain code that will be executed for every Case matched by filter + * @param processIdentifier identifier of PetriNet, to filter which cases should be updated + * @param pageSize Optional attribute to set page size. Default page size 100.0 + */ + void updateCasesCursor(Closure update, String processIdentifier, int pageSize = caseMigrationProperties.pageSize) { + Query query = Query.query(Criteria.where("processIdentifier").is(processIdentifier)) + long caseCount = mongoTemplate.count(query, Case.class) + + if (caseCount > 0) { + long numOfPages = ((caseCount / pageSize) + 1) as long + long page = 1, currentBatchSize = 0; + log.info("Migrating process $processIdentifier") + log.info("Page size: $pageSize") + log.info("Processing cases: $numOfPages pages") + + query.cursorBatchSize(pageSize) + query.with(Sort.by(Sort.Direction.ASC, "_id")) + BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, Case.class) + + try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { + while (cursor.hasNext()) { + prepareUpdateOperation(cursor.next(), update, bulkOps) + if (++currentBatchSize == pageSize as long || !cursor.hasNext()) { + log.info("Updated case page {} / {}", page, numOfPages) + handleBulkOps(bulkOps) + bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, Case.class) + currentBatchSize = 0 + page++ + } + } + } + } + } + + /** + * Update all cases. + * @param update Instance of Closure, which should contain code that will be executed for every Case + * @param pageSize Optional attribute to set page size. Default page size 100.0 + */ + void updateAllCasesCursor(Closure update, double pageSize = 100.0) { + long caseCount = caseRepository.count() + long numOfPages = ((caseCount / pageSize) + 1) as long + log.info("Page size: $pageSize") + log.info("Processing cases: $numOfPages pages") + ObjectId lastId = null + if (caseCount > 0) { + for (int p = 0; p < numOfPages; p++) { + try { + log.info("Page " + (p + 1) + " / $numOfPages") + + Query query = new Query() + if (lastId == null) { + query.skip(0) + } else { + query.addCriteria(Criteria.where("_id").gt(lastId)) + } + query.limit(pageSize as Integer) + + List cases = mongoTemplate.find(query, Case.class) + cases.each { update(it) } + cases = caseRepository.saveAll(cases) + + lastId = cases.get(cases.size() - 1).get_id() + } catch (ArrayIndexOutOfBoundsException e) { + log.error("Failed to iterate page " + (p + 1)) + break + } + } + } + } + + private static void prepareUpdateOperation(Case useCase, Closure update, BulkOperations bulkOps) { + log.debug("Updating case with ID ${useCase.stringId}") + log.trace("Updating case ${useCase.toString()}") + update(useCase) + bulkOps.replaceOne(Query.query(Criteria.where("_id").is(useCase.get_id())), useCase) + } + + private static void handleBulkOps(BulkOperations bulkOps) { + try { + BulkWriteResult bulkWriteResult = bulkOps.execute() + log.debug("Processed bulk write of ${bulkWriteResult.modifiedCount}") + } catch (BulkWriteException e) { + log.error("Failed to write bulk operation", e.getMessage()) + e.getWriteErrors().forEach { + log.error("Error writing document with ID ${it.toString()}. Cause: ${it.getMessage()}") + } + } + } +} diff --git a/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy b/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy new file mode 100644 index 00000000000..635e482e4de --- /dev/null +++ b/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy @@ -0,0 +1,126 @@ +package com.netgrif.application.engine.migration + +import com.netgrif.application.engine.TestHelper +import com.netgrif.application.engine.migration.helpers.CaseMigrationHelper +import com.netgrif.application.engine.petrinet.domain.PetriNet +import com.netgrif.application.engine.petrinet.domain.VersionType +import com.netgrif.application.engine.petrinet.domain.version.Version +import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService +import com.netgrif.application.engine.startup.SuperCreator +import com.netgrif.application.engine.workflow.domain.Case +import com.netgrif.application.engine.workflow.domain.eventoutcomes.petrinetoutcomes.ImportPetriNetEventOutcome +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService +import groovy.util.logging.Slf4j +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.junit.jupiter.SpringExtension + +import java.time.Duration +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.Period; + +@Slf4j +@SpringBootTest +@ActiveProfiles(["test"]) +@ExtendWith(SpringExtension.class) +class MigrationTest { + + @Autowired + private TestHelper testHelper + + @Autowired + private IPetriNetService petriNetService + + @Autowired + private SuperCreator superCreator + + @Autowired + private IWorkflowService workflowService + + @Autowired + private CaseMigrationHelper caseMigrationHelper + + @Autowired + private MigrationHelper migrationHelper + + private PetriNet netV1, netV2 + + private static FileWriter writer + + @BeforeAll + static void beforeAll() { + File report = new File("src/main/resources/migration_report.txt") + if (report.createNewFile()) { + log.info("New migration report file created") + } + writer = new FileWriter(report) + } + + @BeforeEach + void beforeEach() { + testHelper.truncateDbs() + + ImportPetriNetEventOutcome netV1Outcome = petriNetService.importPetriNet(new FileInputStream("src/test/resources/nae_2432_v1.xml"), VersionType.MAJOR, superCreator.getLoggedSuper()) + assert netV1Outcome.getNet() != null + netV1 = netV1Outcome.getNet() + + ImportPetriNetEventOutcome netV2Outcome = petriNetService.importPetriNet(new FileInputStream("src/test/resources/nae_2432_v2.xml"), VersionType.MAJOR, superCreator.getLoggedSuper()) + assert netV2Outcome.getNet() != null + netV2 = netV2Outcome.getNet() + + (1..20000).stream().parallel().forEach { + workflowService.createCase(netV1.stringId, "Net V1 " + it, null, superCreator.loggedSuper, Locale.default) + } + } + + @AfterAll + static void afterAll() { + writer.close() + } + + //TODO: to be deleted + @Test + void migrateCasesWitLegacyCursor() { + LocalDateTime startOfLegacyMigration = LocalDateTime.now() + migrationHelper.updateCasesCursor({ Case useCase -> + migrationHelper.updateCasePermissionsFromNet(useCase, netV2) + migrationHelper.updateTasksPermissions(useCase, netV2, ["person_info", "delete_person", "reset_person", "recreate_person"]) + migrationHelper.reloadTasks(useCase, netV2) + }, "nae_2432") + LocalDateTime endOfLegacyMigration = LocalDateTime.now() + Duration diff = Duration.between(startOfLegacyMigration, endOfLegacyMigration) + writer.write("==============================\n") + writer.write("LEGACY MIGRATION HELPER\n") + writer.write("Migrated 10000 cases\n") + writer.write("Started at " + startOfLegacyMigration.toString() + "\n") + writer.write("Ended at " + endOfLegacyMigration.toString() + "\n") + writer.write("Duration: " + diff.toString() + "\n") + writer.write("==============================\n") + } + + @Test + void migrateCasesWithCursor() { + LocalDateTime startOfLegacyMigration = LocalDateTime.now() + caseMigrationHelper.updateCasesCursor({ Case useCase -> + migrationHelper.updateCasePermissionsFromNet(useCase, netV2) + migrationHelper.updateTasksPermissions(useCase, netV2, ["person_info", "delete_person", "reset_person", "recreate_person"]) + migrationHelper.reloadTasks(useCase, netV2) + }, "nae_2432") + LocalDateTime endOfLegacyMigration = LocalDateTime.now() + Duration diff = Duration.between(startOfLegacyMigration, endOfLegacyMigration) + writer.write("==============================\n") + writer.write("NEW MIGRATION HELPER\n") + writer.write("Migrated 10000 cases\n") + writer.write("Started at " + startOfLegacyMigration.toString() + "\n") + writer.write("Ended at " + endOfLegacyMigration.toString() + "\n") + writer.write("Duration: " + diff.toString() + "\n") + writer.write("==============================\n") + } +} diff --git a/src/test/resources/nae_2432_v1.xml b/src/test/resources/nae_2432_v1.xml new file mode 100644 index 00000000000..fc98584acb4 --- /dev/null +++ b/src/test/resources/nae_2432_v1.xml @@ -0,0 +1,257 @@ + + nae_2432 + 1.0.0 + NAE + NAE-2432 + mic + true + true + false + NAE-2432 + + delete_info_text + + <init><h1>Finish this task to delete person.</h1></init> + </data> + <data type="text"> + <id>first_name</id> + <title>First name + John + First name of person + + + last_name + Last name + Doe + Last name of person + + + note + Note + Example note + Notes about this person + + + reset_info_text + + <init><h1>Finish task to reset this person.</h1></init> + </data> + <transition> + <id>delete_person</id> + <x>816</x> + <y>176</y> + <label>Delete person</label> + <icon>delete</icon> + <dataGroup> + <id>delete_person_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>delete_info_text</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>htmltextarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>person_info</id> + <x>528</x> + <y>176</y> + <label>Person info</label> + <icon>person</icon> + <dataGroup> + <id>t1_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>first_name</id> + <logic> + <behavior>editable</behavior> + <behavior>required</behavior> + <behavior>immediate</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>last_name</id> + <logic> + <behavior>editable</behavior> + <behavior>required</behavior> + <behavior>immediate</behavior> + </logic> + <layout> + <x>2</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>note</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>1</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>textarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>reset_person</id> + <x>816</x> + <y>304</y> + <label>Reset person</label> + <icon>reset_tv</icon> + <dataGroup> + <id>reset_person_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>reset_info_text</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>htmltextarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <place> + <id>p1</id> + <x>336</x> + <y>176</y> + <tokens>1</tokens> + <static>false</static> + </place> + <place> + <id>p2</id> + <x>656</x> + <y>176</y> + <tokens>0</tokens> + <static>false</static> + </place> + <place> + <id>p3</id> + <x>656</x> + <y>304</y> + <tokens>0</tokens> + <static>false</static> + </place> + <place> + <id>p4</id> + <x>976</x> + <y>176</y> + <tokens>1</tokens> + <static>false</static> + </place> + <arc> + <id>a1</id> + <type>regular</type> + <sourceId>p1</sourceId> + <destinationId>person_info</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a2</id> + <type>regular</type> + <sourceId>person_info</sourceId> + <destinationId>p2</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a3</id> + <type>regular</type> + <sourceId>person_info</sourceId> + <destinationId>p3</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a4</id> + <type>regular</type> + <sourceId>p2</sourceId> + <destinationId>delete_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a5</id> + <type>regular</type> + <sourceId>p3</sourceId> + <destinationId>reset_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a6</id> + <type>regular</type> + <sourceId>delete_person</sourceId> + <destinationId>p4</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a7</id> + <type>regular</type> + <sourceId>reset_person</sourceId> + <destinationId>p1</destinationId> + <multiplicity>1</multiplicity> + <breakpoint> + <x>816</x> + <y>368</y> + </breakpoint> + <breakpoint> + <x>336</x> + <y>368</y> + </breakpoint> + </arc> + <arc> + <id>a8</id> + <type>regular</type> + <sourceId>p2</sourceId> + <destinationId>reset_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a9</id> + <type>regular</type> + <sourceId>p3</sourceId> + <destinationId>delete_person</destinationId> + <multiplicity>1</multiplicity> + </arc> +</document> \ No newline at end of file diff --git a/src/test/resources/nae_2432_v2.xml b/src/test/resources/nae_2432_v2.xml new file mode 100644 index 00000000000..b1cab7bc21f --- /dev/null +++ b/src/test/resources/nae_2432_v2.xml @@ -0,0 +1,388 @@ +<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.com/petriflow.schema.xsd"> + <id>nae_2432</id> + <version>1.0.0</version> + <initials>NAE</initials> + <title>NAE-2432 + mic + true + true + false + NAE-2432 + + data_editor + Data editor + + + person_creator + Person creator + + + person_recreator + Person recreator + + + person_remover + Person remover + + + person_reseter + Person resetter + + + delete_info_text + + <init><h1>Finish this task to delete person.</h1></init> + </data> + <data type="text"> + <id>first_name</id> + <title>First name + John + First name of person + + + income + Income + Income of person + 1000 + + + last_name + Last name + Doe + Last name of person + + + note + Note + Example note + Notes about this person + + + recreate_info_text + + <init><h1>Finish this task to recreate person.</h1></init> + </data> + <data type="text"> + <id>reset_info_text</id> + <title/> + <init><h1>Finish task to reset this person.</h1></init> + </data> + <transition> + <id>delete_person</id> + <x>816</x> + <y>176</y> + <label>Delete person</label> + <icon>delete</icon> + <roleRef> + <id>person_remover</id> + <logic> + <view>true</view> + <perform>true</perform> + </logic> + </roleRef> + <dataGroup> + <id>delete_person_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>delete_info_text</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>htmltextarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>person_info</id> + <x>528</x> + <y>176</y> + <label>Person info</label> + <icon>person</icon> + <roleRef> + <id>data_editor</id> + <logic> + <view>true</view> + <perform>true</perform> + </logic> + </roleRef> + <roleRef> + <id>person_creator</id> + <logic> + <view>true</view> + <perform>true</perform> + </logic> + </roleRef> + <dataGroup> + <id>t1_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>first_name</id> + <logic> + <behavior>editable</behavior> + <behavior>required</behavior> + <behavior>immediate</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>last_name</id> + <logic> + <behavior>editable</behavior> + <behavior>required</behavior> + <behavior>immediate</behavior> + </logic> + <layout> + <x>2</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>income</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>1</y> + <rows>1</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>currency</name> + <property key="fractionSize">2</property> + </component> + </dataRef> + <dataRef> + <id>note</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>2</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>textarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>recreate_person</id> + <x>976</x> + <y>48</y> + <label>Recreate person</label> + <icon>emergency</icon> + <roleRef> + <id>person_recreator</id> + <logic> + <view>true</view> + <perform>true</perform> + </logic> + </roleRef> + <dataGroup> + <id>recreate_person_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>recreate_info_text</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>htmltextarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>reset_person</id> + <x>816</x> + <y>304</y> + <label>Reset person</label> + <icon>reset_tv</icon> + <roleRef> + <id>person_reseter</id> + <logic> + <view>true</view> + <perform>true</perform> + </logic> + </roleRef> + <dataGroup> + <id>reset_person_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>reset_info_text</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>htmltextarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <place> + <id>p1</id> + <x>336</x> + <y>176</y> + <tokens>1</tokens> + <static>false</static> + </place> + <place> + <id>p2</id> + <x>656</x> + <y>176</y> + <tokens>0</tokens> + <static>false</static> + </place> + <place> + <id>p3</id> + <x>656</x> + <y>304</y> + <tokens>0</tokens> + <static>false</static> + </place> + <place> + <id>p4</id> + <x>976</x> + <y>176</y> + <tokens>0</tokens> + <static>false</static> + </place> + <arc> + <id>a1</id> + <type>regular</type> + <sourceId>p1</sourceId> + <destinationId>person_info</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a10</id> + <type>regular</type> + <sourceId>p4</sourceId> + <destinationId>recreate_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a11</id> + <type>regular</type> + <sourceId>recreate_person</sourceId> + <destinationId>p1</destinationId> + <multiplicity>1</multiplicity> + <breakpoint> + <x>336</x> + <y>48</y> + </breakpoint> + </arc> + <arc> + <id>a2</id> + <type>regular</type> + <sourceId>person_info</sourceId> + <destinationId>p2</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a3</id> + <type>regular</type> + <sourceId>person_info</sourceId> + <destinationId>p3</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a4</id> + <type>regular</type> + <sourceId>p2</sourceId> + <destinationId>delete_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a5</id> + <type>regular</type> + <sourceId>p3</sourceId> + <destinationId>reset_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a6</id> + <type>regular</type> + <sourceId>delete_person</sourceId> + <destinationId>p4</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a7</id> + <type>regular</type> + <sourceId>reset_person</sourceId> + <destinationId>p1</destinationId> + <multiplicity>1</multiplicity> + <breakpoint> + <x>816</x> + <y>368</y> + </breakpoint> + <breakpoint> + <x>336</x> + <y>368</y> + </breakpoint> + </arc> + <arc> + <id>a8</id> + <type>regular</type> + <sourceId>p2</sourceId> + <destinationId>reset_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a9</id> + <type>regular</type> + <sourceId>p3</sourceId> + <destinationId>delete_person</destinationId> + <multiplicity>1</multiplicity> + </arc> +</document> \ No newline at end of file From 232956d51197bf61c32dfa296f41df1597c654bc Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Wed, 13 May 2026 07:02:17 +0200 Subject: [PATCH 02/20] Refactor and add migration helpers for streamlined operations Introduced abstract and task-specific migration helpers to enhance reusability and efficiency of MongoDB migration logic. Improved test coverage with `MigrationBenchmarkTest`, consolidated redundant methods, and standardized operations for cases, tasks, and PetriNet migrations. --- .../MigrationConfigurationProperties.groovy | 33 ++- .../helpers/AbstractMigrationHelper.groovy | 143 ++++++++++++ .../helpers/CaseMigrationHelper.groovy | 216 ++++++------------ .../helpers/PetriNetMigrationHelper.groovy | 45 ++++ .../helpers/TaskMigrationHelper.groovy | 142 ++++++++++++ .../engine/mongodb/MongodbSerializer.java | 18 ++ .../engine/utils/MongodbUtils.java | 15 ++ ...t.groovy => MigrationBenchmarkTest.groovy} | 11 +- 8 files changed, 473 insertions(+), 150 deletions(-) create mode 100644 src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy create mode 100644 src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy create mode 100644 src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy create mode 100644 src/main/java/com/netgrif/application/engine/mongodb/MongodbSerializer.java create mode 100644 src/main/java/com/netgrif/application/engine/utils/MongodbUtils.java rename src/test/groovy/com/netgrif/application/engine/migration/{MigrationTest.groovy => MigrationBenchmarkTest.groovy} (88%) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy b/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy index 15f4fe3a4b3..1813b2eec30 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy @@ -1,7 +1,6 @@ package com.netgrif.application.engine.migration.config.properties import lombok.Data -import lombok.Getter import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.stereotype.Component @@ -11,6 +10,10 @@ class MigrationConfigurationProperties { private CaseMigrationProperties cases = new CaseMigrationProperties() + private TaskMigrationProperties tasks = new TaskMigrationProperties() + + private PetriNetMigrationProperties petriNets = new PetriNetMigrationProperties() + @Data static class CaseMigrationProperties { @@ -21,7 +24,35 @@ class MigrationConfigurationProperties { } } + @Data + static class TaskMigrationProperties { + + private int pageSize = 100 + + int getPageSize() { + return pageSize + } + } + + @Data + static class PetriNetMigrationProperties { + + private int pageSize = 100 + + int getPageSize() { + return pageSize + } + } + CaseMigrationProperties getCases() { return cases } + + TaskMigrationProperties getTasks() { + return tasks + } + + PetriNetMigrationProperties getPetriNets() { + return petriNets + } } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy new file mode 100644 index 00000000000..15d363a7e45 --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy @@ -0,0 +1,143 @@ +package com.netgrif.application.engine.migration.helpers + +import com.mongodb.BulkWriteException +import com.mongodb.bulk.BulkWriteResult +import com.netgrif.application.engine.utils.MongodbUtils +import com.netgrif.application.engine.workflow.domain.Case +import com.querydsl.core.types.Predicate +import groovy.util.logging.Slf4j +import org.springframework.data.mongodb.core.BulkOperations +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.util.CloseableIterator + +/** + * AbstractMigrationHelper is an abstract utility class to facilitate the bulk migration of + * MongoDB documents. The class provides mechanisms for iterating over documents, preparing + * bulk migration operations, and executing those operations efficiently using Spring Data MongoDB's + * BulkOperations. It is generic and requires the subtype (document type) to be specified. + * + * @param <T> The type of documents this helper will operate on. + */ +@Slf4j +abstract class AbstractMigrationHelper<T> { + + /** + * Default Closure used to process bulk operations. It uses the {@link #handleBulkOps} method + * to safely execute the bulk operations and log results or errors. + */ + static final Closure DEFAULT_PROCESS_OPERATIONS = { BulkOperations bulkOperations -> handleBulkOps(bulkOperations) } + + + /** + * The type of the documents this helper is operating on. + * It is expected to be provided by subclasses, as the class itself is generic and requires + * specific document type initialization to perform the corresponding operations. + */ + private Class<T> type + + /** + * The {@link MongoTemplate} used for interacting with the MongoDB database. + * This is the core dependency of the helper class, allowing it to execute queries, + * bulk operations, and other database operations on the specified document type. + */ + private MongoTemplate mongoTemplate + + /** + * Constructs a new AbstractMigrationHelper with the specified MongoTemplate. + * + * @param mongoTemplate the {@link MongoTemplate} to use for interacting with MongoDB + */ + AbstractMigrationHelper(Class<T> type, MongoTemplate mongoTemplate) { + this.type = type + this.mongoTemplate = mongoTemplate + } + + /** + * Returns the page size that should be used for iterating over documents. + * + * @return the number of documents per page + */ + abstract int getPageSize() + + /** + * Prepares bulk operations on a single document. + * This method must be implemented by subclasses to define the specific bulk operations + * to perform on each document. + * + * @param document the document to process + * @param update the Closure defining the update operation + * @param bulkOperations the {@link BulkOperations} instance to add operations to + */ + abstract void prepareOperations(T document, Closure update, BulkOperations bulkOperations) + + /** + * A static method to handle the execution of bulk operations. + * It executes the given {@link BulkOperations} instance and logs the results or any errors. + * + * @param bulkOps the bulk operations to execute + */ + static void handleBulkOps(BulkOperations bulkOps) { + try { + BulkWriteResult bulkWriteResult = bulkOps.execute() + log.debug("Processed bulk write of ${bulkWriteResult.modifiedCount}") + } catch (BulkWriteException e) { + log.error("Failed to write bulk operation", e.getMessage()) + e.getWriteErrors().forEach { + log.error("Error writing document with ID ${it.toString()}. Cause: ${it.getMessage()}") + } + } + } + + /** + * Converts a QueryDSL {@link Predicate} to a MongoDB {@link Query}. + * + * @param predicate the QueryDSL predicate to convert + * @return a MongoDB Query object representing the predicate + */ + protected Query toQuery(Predicate predicate) { + return MongodbUtils.toQuery(mongoTemplate, type, predicate) + } + + /** + * Iterates over the documents in the collection, applies updates, and executes bulk operations. + * The iteration is paginated based on the provided or default page size, and supports customizable + * bulk operation processing and optional sleep intervals between pages. + * + * @param update a {@link Closure} defining the update to apply to documents + * @param processOperations an optional {@link Closure} to process bulk operations; defaults + * to {@link #DEFAULT_PROCESS_OPERATIONS} + * @param query an optional MongoDB {@link Query} to filter documents; defaults to an empty query + * @param sleepFor the optional number of milliseconds to sleep between processing pages; defaults to 0 + * @param pageSize the size of each page (number of documents); defaults to the result of {@link #getPageSize()} + */ + void iterate(Closure update, Closure processOperations = DEFAULT_PROCESS_OPERATIONS, + Query query = new Query(), long sleepFor = 0, int pageSize = getPageSize()) { + long count = mongoTemplate.count(query, type) + if (count > 0) { + long numOfPages = ((count / pageSize) + 1) as long + log.info("Processing ${type.getSimpleName()} documents with filter ${query.toString()}: $numOfPages pages") + + long page = 1, currentBatchSize = 0 + query.cursorBatchSize(pageSize) + BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, Case.class) + + try (CloseableIterator<T> cursor = mongoTemplate.stream(query, type)) { + while (cursor.hasNext()) { + prepareOperations(cursor.next(), update, bulkOps) + if (++currentBatchSize == pageSize as long || !cursor.hasNext()) { + log.info("Processed ${type.getSimpleName()} document page {} / {}", page, numOfPages) + processOperations(bulkOps) + bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, Case.class) + currentBatchSize = 0 + page++ + if (sleepFor > 0) { + log.debug("Pausing migration for ${sleepFor} milliseconds") + sleep(sleepFor) + } + } + } + } + } + } +} diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index e350e21775d..54dc939930e 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -1,181 +1,117 @@ package com.netgrif.application.engine.migration.helpers -import com.mongodb.BulkWriteError -import com.mongodb.BulkWriteException -import com.mongodb.bulk.BulkWriteResult import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.CaseMigrationProperties import com.netgrif.application.engine.workflow.domain.Case -import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository import com.querydsl.core.types.Predicate import groovy.util.logging.Slf4j -import lombok.RequiredArgsConstructor -import org.bson.types.ObjectId -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.data.domain.Page -import org.springframework.data.domain.PageRequest -import org.springframework.data.domain.Sort import org.springframework.data.mongodb.core.BulkOperations import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Criteria import org.springframework.data.mongodb.core.query.Query -import org.springframework.data.util.CloseableIterator import org.springframework.stereotype.Component +/** + * Helper class for managing migrations of Case objects in the application. + * Provides methods for updating and iterating over case objects, filtered + * by specified conditions, and applying custom update logic using closures. + * + * This class extends {@link AbstractMigrationHelper} and utilizes MongoDB + * for operations on the data. + */ @Slf4j @Component -@RequiredArgsConstructor -class CaseMigrationHelper { - - public static final String CASE_COLLECTION_NAME = "case" - - private MongoTemplate mongoTemplate - - private CaseRepository caseRepository +class CaseMigrationHelper extends AbstractMigrationHelper<Case> { + /** + * Configuration properties for case migration. + */ private CaseMigrationProperties caseMigrationProperties - @Autowired - void setMongoTemplate(MongoTemplate mongoTemplate) { - this.mongoTemplate = mongoTemplate + /** + * Constructs a CaseMigrationHelper instance with + * the provided MongoTemplate and migration configuration properties. + * + * @param mongoTemplate MongoTemplate to interact with MongoDB. + * @param migrationConfigurationProperties Properties for migration configuration, including cases. + */ + CaseMigrationHelper(MongoTemplate mongoTemplate, + MigrationConfigurationProperties migrationConfigurationProperties) { + super(Case.class, mongoTemplate) + this.caseMigrationProperties = migrationConfigurationProperties.cases } - @Autowired - void setCaseRepository(CaseRepository caseRepository) { - this.caseRepository = caseRepository + /** + * Retrieves the configured page size for batch processing of cases. + * + * @return the page size for case processing. + */ + @Override + int getPageSize() { + return caseMigrationProperties.pageSize } - @Autowired - void setCaseMigrationProperties(MigrationConfigurationProperties migrationConfigurationProperties) { - this.caseMigrationProperties = migrationConfigurationProperties.cases + /** + * Prepares bulk operations for updating a case. The provided update closure + * is executed to modify the case, and a replace operation is created. + * + * @param useCase The case object to update. + * @param update A closure containing the update logic to be applied to the case. + * @param bulkOperations BulkOperations instance used to queue updates for batch processing. + */ + @Override + void prepareOperations(Case useCase, Closure update, BulkOperations bulkOperations) { + log.debug("Updating case with ID ${useCase.stringId}") + log.trace("Updating case ${useCase.toString()}") + update(useCase) + bulkOperations.replaceOne(Query.query(Criteria.where("_id").is(useCase.get_id())), useCase) } -/** - * Updates all cases filtered by filter Predicate. Update closure is called on each filtered case. - * @param update Instance of Closure, which should contain code that will be executed for every Case matched by filter - * @param filter Instance of Predicate, to filter which cases should be updated + + /** + * Updates all cases that match the given filter predicate. The update closure + * is executed for each matched case. + * + * @param update A closure containing the code to execute for each matching case. + * @param filter A QueryDSL Predicate object specifying the conditions to filter the cases. */ void updateCases(Closure update, Predicate filter) { log.info("Updating cases with filter ${filter.toString()} and update ${update.toString()}") - iterateCases(update, { Page<Case> cases -> caseRepository.saveAll(cases) }, filter) + iterate(update, DEFAULT_PROCESS_OPERATIONS, toQuery(filter)) } /** - * Iterates all cases filtered by filter Predicate. Update closure is called on each filtered case. PageProcessed closure is called after each page iteration. - * @param update Instance of Closure, which should contain code that will be executed for every Case matched by filter (changes made to Case will not be saved automatically, for that use updateCases method) - * @param sleepFor Optional attribute to set sleep time (in milliseconds) to sleep for after each iterated page. Default 0ms - * @param filter Instance of Predicate, to filter which cases should be iterated + * Iterates over all cases that match the given filter predicate. The update closure + * is executed for each matched case, and the pageProcessed closure is called after each page. + * + * @param update A closure containing the code to execute for each matching case. + * @param pageProcessed A closure executed after processing each page. Defaults to DEFAULT_PROCESS_OPERATIONS. + * @param sleepFor Optional sleep time (in milliseconds) between processing pages. Default is 0ms. + * @param filter A QueryDSL Predicate object specifying the conditions to filter the cases. */ - void iterateCases(Closure update, Closure pageProcessed = {}, long sleepFor = 0, Predicate filter) { - long caseCount = caseRepository.count(filter) - long numOfPages = ((caseCount / caseMigrationProperties.pageSize) + 1) as long - log.info("Processing cases with filter ${filter.toString()}: $numOfPages pages") - numOfPages.times { page -> - log.info("Page $page / $numOfPages") - - Page<Case> cases = caseRepository.findAll(filter, PageRequest.of(page, caseMigrationProperties.pageSize)) - - cases.each { - log.debug("Processing case with id ${it.stringId}") - log.trace("Processing case ${it.toString()}") - update(it) - } - pageProcessed(cases) - if (sleepFor != 0) { - log.debug("Pausing migration for ${sleepFor} milliseconds") - sleep(sleepFor) - } - } + void iterateCases(Closure update, Closure pageProcessed = DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { + iterate(update, pageProcessed, toQuery(filter), sleepFor) } /** - * Updates all cases of a given process. - * @param update Instance of Closure, which should contain code that will be executed for every Case matched by filter - * @param processIdentifier identifier of PetriNet, to filter which cases should be updated - * @param pageSize Optional attribute to set page size. Default page size 100.0 + * Updates all cases of a specific process identified by its process identifier. + * + * @param update A closure containing the code to execute for each matching case. + * @param processIdentifier The identifier of the PetriNet process. + * @param pageSize Optional page size for processing cases. Default is 100.0. */ - void updateCasesCursor(Closure update, String processIdentifier, int pageSize = caseMigrationProperties.pageSize) { - Query query = Query.query(Criteria.where("processIdentifier").is(processIdentifier)) - long caseCount = mongoTemplate.count(query, Case.class) - - if (caseCount > 0) { - long numOfPages = ((caseCount / pageSize) + 1) as long - long page = 1, currentBatchSize = 0; - log.info("Migrating process $processIdentifier") - log.info("Page size: $pageSize") - log.info("Processing cases: $numOfPages pages") - - query.cursorBatchSize(pageSize) - query.with(Sort.by(Sort.Direction.ASC, "_id")) - BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, Case.class) - - try (CloseableIterator<Case> cursor = mongoTemplate.stream(query, Case.class)) { - while (cursor.hasNext()) { - prepareUpdateOperation(cursor.next(), update, bulkOps) - if (++currentBatchSize == pageSize as long || !cursor.hasNext()) { - log.info("Updated case page {} / {}", page, numOfPages) - handleBulkOps(bulkOps) - bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, Case.class) - currentBatchSize = 0 - page++ - } - } - } - } + void updateCasesCursor(Closure update, String processIdentifier, double pageSize = 100.0) { + Query query = new Query(Criteria.where("processIdentifier").is(processIdentifier)) + iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int) } /** - * Update all cases. - * @param update Instance of Closure, which should contain code that will be executed for every Case - * @param pageSize Optional attribute to set page size. Default page size 100.0 + * Updates all cases in the system. The update closure is executed for each case. + * + * @param update A closure containing the code to execute for each case. + * @param pageSize Optional page size for processing cases. Default is 100.0. */ void updateAllCasesCursor(Closure update, double pageSize = 100.0) { - long caseCount = caseRepository.count() - long numOfPages = ((caseCount / pageSize) + 1) as long - log.info("Page size: $pageSize") - log.info("Processing cases: $numOfPages pages") - ObjectId lastId = null - if (caseCount > 0) { - for (int p = 0; p < numOfPages; p++) { - try { - log.info("Page " + (p + 1) + " / $numOfPages") - - Query query = new Query() - if (lastId == null) { - query.skip(0) - } else { - query.addCriteria(Criteria.where("_id").gt(lastId)) - } - query.limit(pageSize as Integer) - - List<Case> cases = mongoTemplate.find(query, Case.class) - cases.each { update(it) } - cases = caseRepository.saveAll(cases) - - lastId = cases.get(cases.size() - 1).get_id() - } catch (ArrayIndexOutOfBoundsException e) { - log.error("Failed to iterate page " + (p + 1)) - break - } - } - } - } - - private static void prepareUpdateOperation(Case useCase, Closure update, BulkOperations bulkOps) { - log.debug("Updating case with ID ${useCase.stringId}") - log.trace("Updating case ${useCase.toString()}") - update(useCase) - bulkOps.replaceOne(Query.query(Criteria.where("_id").is(useCase.get_id())), useCase) - } - - private static void handleBulkOps(BulkOperations bulkOps) { - try { - BulkWriteResult bulkWriteResult = bulkOps.execute() - log.debug("Processed bulk write of ${bulkWriteResult.modifiedCount}") - } catch (BulkWriteException e) { - log.error("Failed to write bulk operation", e.getMessage()) - e.getWriteErrors().forEach { - log.error("Error writing document with ID ${it.toString()}. Cause: ${it.getMessage()}") - } - } + iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int) } } + diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy new file mode 100644 index 00000000000..1bbf23f8bb1 --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy @@ -0,0 +1,45 @@ +package com.netgrif.application.engine.migration.helpers + +import com.netgrif.application.engine.AsyncRunner +import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties +import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.PetriNetMigrationProperties +import com.netgrif.application.engine.petrinet.domain.PetriNet +import groovy.util.logging.Slf4j +import org.springframework.data.mongodb.core.BulkOperations +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.stereotype.Component + +@Slf4j +@Component +class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { + + private PetriNetMigrationProperties petriNetMigrationProperties + + /** + * Constructs a new PetriNetMigrationHelper with the specified MongoTemplate. + * + * @param mongoTemplate the {@link MongoTemplate} to use for interacting with MongoDB + */ + PetriNetMigrationHelper(MongoTemplate mongoTemplate, + MigrationConfigurationProperties migrationConfigurationProperties) { + super(PetriNet.class, mongoTemplate) + this.petriNetMigrationProperties = migrationConfigurationProperties.petriNets + } + + @Override + int getPageSize() { + return petriNetMigrationProperties.pageSize + } + + @Override + void prepareOperations(PetriNet document, Closure update, BulkOperations bulkOperations) { + log.debug("Updating case with ID ${document.stringId}") + log.trace("Updating case ${document.toString()}") + update(document) + bulkOperations.replaceOne(Query.query(Criteria.where("_id").is(document.getObjectId())), document) + } + + +} diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy new file mode 100644 index 00000000000..d23eab7dbda --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy @@ -0,0 +1,142 @@ +package com.netgrif.application.engine.migration.helpers + +import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties +import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.TaskMigrationProperties +import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService +import com.netgrif.application.engine.workflow.domain.Task +import com.querydsl.core.types.Predicate +import groovy.util.logging.Slf4j +import org.springframework.data.mongodb.core.BulkOperations +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.stereotype.Component +/** + * A helper class for managing task migrations. + * This class extends {@link AbstractMigrationHelper} and provides methods for updating, iterating, + * and manipulating {@link Task} entities in bulk during migration processes. + * It integrates with MongoDB and uses the {@link MongoTemplate} for data operations and + * {@link IPetriNetService} for interacting with PetriNet services. + */ +@Slf4j +@Component +class TaskMigrationHelper extends AbstractMigrationHelper<Task>{ + + /** + * The task migration properties configuration. + * + * This property provides the configuration values for task migration, + * such as the size of the page used to process tasks in the migration. + * It is loaded from the {@link MigrationConfigurationProperties} during initialization. + */ + private TaskMigrationProperties taskMigrationProperties + + /** + * Service for handling Petri Net operations. + * + * This service is used to access and interact with Petri Net tasks, + * such as retrieving the latest version of a Petri Net by its identifier + * during task migrations. + */ + private IPetriNetService petriNetService + + /** + * Constructs a new TaskMigrationHelper with the specified MongoTemplate. + * + * @param mongoTemplate the {@link MongoTemplate} to use for interacting with MongoDB + */ + TaskMigrationHelper(MongoTemplate mongoTemplate, + MigrationConfigurationProperties migrationConfigurationProperties, + IPetriNetService petriNetService) { + super(Task.class, mongoTemplate) + this.taskMigrationProperties = migrationConfigurationProperties.tasks + this.petriNetService = petriNetService + } + + /** + * Returns the page size for the task migration process. + * + * The page size is configured in the {@link TaskMigrationProperties} and determines + * the number of tasks processed in a single batch during migration operations. + * + * @return an integer indicating the configured page size + */ + @Override + int getPageSize() { + return taskMigrationProperties.pageSize + } + + /** + * Prepares a set of bulk operations for tasks during the migration process. + * + * This method is called for each individual {@link Task} document that needs to be updated. + * It executes the provided {@code update} closure to modify the task and + * prepares a bulk replacement operation to save the changes to the database. + * + * @param document the {@link Task} document to be updated + * @param update a {@link Closure} that defines the update logic to be applied to the {@link Task} + * @param bulkOperations the {@link BulkOperations} object used to queue the MongoDB operations for batch execution + */ + @Override + void prepareOperations(Task document, Closure update, BulkOperations bulkOperations) { + log.debug("Updating case with ID ${document.stringId}") + log.trace("Updating case ${document.toString()}") + update(document) + bulkOperations.replaceOne(Query.query(Criteria.where("_id").is(document.getObjectId())), document) + } + + /** + * Updates all tasks filtered by filter Predicate. Update closure is called on each filtered task. + * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter + * @param filter Instance of Predicate, to filter which tasks should be updated + */ + void updateTasks(Closure update, Predicate filter) { + log.info("Updating tasks with filter ${filter.toString()} and update ${update.toString()}") + iterate(update, DEFAULT_PROCESS_OPERATIONS, toQuery(filter)) + } + + /** + * Iterates all tasks filtered by filter Predicate. Update closure is called on each filtered task. PageProcessed closure is called after each page iteration. + * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter (changes made to Task will not be saved automatically, for that use updateCases method) + * @param sleepFor Optional attribute to set sleep time (in milliseconds) to sleep for after each iterated page. Default 0ms + * @param filter Instance of Predicate, to filter which tasks should be iterated + */ + void iterateTasks(Closure update, Closure pageProcessed = DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { + iterate(update, pageProcessed, toQuery(filter), sleepFor) + } + + /** + * Updates all tasks of a given process. + * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter + * @param processIdentifier identifier of PetriNet, to filter which tasks should be updated + * @param pageSize Optional attribute to set page size. Default page size 100.0 + */ + void updateTasksCursor(Closure update, String processIdentifier, double pageSize = 100.0) { + String processId = petriNetService.getNewestVersionByIdentifier(processIdentifier).stringId + Query query = new Query(Criteria.where("processId").is(processId)) + iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int) + } + + /** + * Updates specific tasks of a given process. + * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter + * @param processIdentifier identifier of PetriNet, to filter which tasks should be updated + * @param transitionIds List of transition IDs to limit filter to specific transitions of given processIdentifier + * @param pageSize Optional attribute to set page size. Default page size 100.0 + */ + void updateSpecificTasksCursor(Closure update, String processIdentifier, List<String> transitionIds, double pageSize = 100.0) { + String processId = petriNetService.getNewestVersionByIdentifier(processIdentifier).stringId + Query query = new Query(Criteria.where("processId").is(processId)) + query.addCriteria(Criteria.where("transitionId").in(transitionIds)) + iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int) + } + + /** + * Update all tasks. + * @param update Instance of Closure, which should contain code that will be executed for every Task + * @param pageSize Optional attribute to set page size. Default page size 100.0 + */ + void updateAllTasksCursor(Closure update, double pageSize = 100.0) { + iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int) + } +} diff --git a/src/main/java/com/netgrif/application/engine/mongodb/MongodbSerializer.java b/src/main/java/com/netgrif/application/engine/mongodb/MongodbSerializer.java new file mode 100644 index 00000000000..a1b38e6736d --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/mongodb/MongodbSerializer.java @@ -0,0 +1,18 @@ +package com.netgrif.application.engine.mongodb; + +import com.mongodb.DBRef; +import com.querydsl.core.types.Path; +import com.querydsl.mongodb.document.MongodbDocumentSerializer; + +public class MongodbSerializer extends MongodbDocumentSerializer { + + @Override + protected DBRef asReference(Object o) { + return null; + } + + @Override + protected boolean isReference(Path<?> path) { + return false; + } +} diff --git a/src/main/java/com/netgrif/application/engine/utils/MongodbUtils.java b/src/main/java/com/netgrif/application/engine/utils/MongodbUtils.java new file mode 100644 index 00000000000..310e825cfb8 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/utils/MongodbUtils.java @@ -0,0 +1,15 @@ +package com.netgrif.application.engine.utils; + +import com.querydsl.core.types.Predicate; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.support.SpringDataMongodbQuery; + +public class MongodbUtils { + + public static <T> Query toQuery(MongoTemplate mongoTemplate, Class<T> type, Predicate... predicate) { + SpringDataMongodbQuery<T> springDataMongodbQuery = new SpringDataMongodbQuery<>(mongoTemplate, type).where(predicate); + return new BasicQuery(springDataMongodbQuery.asDocument()); + } +} diff --git a/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy b/src/test/groovy/com/netgrif/application/engine/migration/MigrationBenchmarkTest.groovy similarity index 88% rename from src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy rename to src/test/groovy/com/netgrif/application/engine/migration/MigrationBenchmarkTest.groovy index 635e482e4de..795b0a64eaa 100644 --- a/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/migration/MigrationBenchmarkTest.groovy @@ -4,7 +4,6 @@ import com.netgrif.application.engine.TestHelper import com.netgrif.application.engine.migration.helpers.CaseMigrationHelper import com.netgrif.application.engine.petrinet.domain.PetriNet import com.netgrif.application.engine.petrinet.domain.VersionType -import com.netgrif.application.engine.petrinet.domain.version.Version import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService import com.netgrif.application.engine.startup.SuperCreator import com.netgrif.application.engine.workflow.domain.Case @@ -22,15 +21,13 @@ import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.junit.jupiter.SpringExtension import java.time.Duration -import java.time.LocalDate import java.time.LocalDateTime -import java.time.Period; @Slf4j @SpringBootTest @ActiveProfiles(["test"]) @ExtendWith(SpringExtension.class) -class MigrationTest { +class MigrationBenchmarkTest { @Autowired private TestHelper testHelper @@ -75,7 +72,7 @@ class MigrationTest { assert netV2Outcome.getNet() != null netV2 = netV2Outcome.getNet() - (1..20000).stream().parallel().forEach { + (1..10000).stream().parallel().forEach { workflowService.createCase(netV1.stringId, "Net V1 " + it, null, superCreator.loggedSuper, Locale.default) } } @@ -91,8 +88,6 @@ class MigrationTest { LocalDateTime startOfLegacyMigration = LocalDateTime.now() migrationHelper.updateCasesCursor({ Case useCase -> migrationHelper.updateCasePermissionsFromNet(useCase, netV2) - migrationHelper.updateTasksPermissions(useCase, netV2, ["person_info", "delete_person", "reset_person", "recreate_person"]) - migrationHelper.reloadTasks(useCase, netV2) }, "nae_2432") LocalDateTime endOfLegacyMigration = LocalDateTime.now() Duration diff = Duration.between(startOfLegacyMigration, endOfLegacyMigration) @@ -110,8 +105,6 @@ class MigrationTest { LocalDateTime startOfLegacyMigration = LocalDateTime.now() caseMigrationHelper.updateCasesCursor({ Case useCase -> migrationHelper.updateCasePermissionsFromNet(useCase, netV2) - migrationHelper.updateTasksPermissions(useCase, netV2, ["person_info", "delete_person", "reset_person", "recreate_person"]) - migrationHelper.reloadTasks(useCase, netV2) }, "nae_2432") LocalDateTime endOfLegacyMigration = LocalDateTime.now() Duration diff = Duration.between(startOfLegacyMigration, endOfLegacyMigration) From 3566675475aa5165379dba35e7cbe40cf4c5725a Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Mon, 18 May 2026 14:32:54 +0200 Subject: [PATCH 03/20] Refactor MigrationHelper to delegate logic to helper classes Streamlined the MigrationHelper class by delegating migration logic to specialized helper classes (CaseMigrationHelper, TaskMigrationHelper, and PetriNetMigrationHelper). This improves code modularity, maintainability, and reduces redundancy by centralizing shared operations. --- .../engine/migration/MigrationHelper.groovy | 517 ++---------------- .../helpers/CaseMigrationHelper.groovy | 153 +++++- .../helpers/PetriNetMigrationHelper.groovy | 354 +++++++++++- .../helpers/TaskMigrationHelper.groovy | 62 ++- .../engine/workflow/domain/DataField.java | 1 + 5 files changed, 615 insertions(+), 472 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy index 3b9c3c3e0cd..f6489f795fd 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy @@ -3,6 +3,10 @@ package com.netgrif.application.engine.migration import com.netgrif.application.engine.auth.service.interfaces.IUserService import com.netgrif.application.engine.elastic.service.interfaces.* import com.netgrif.application.engine.importer.service.Importer +import com.netgrif.application.engine.migration.helpers.AbstractMigrationHelper +import com.netgrif.application.engine.migration.helpers.CaseMigrationHelper +import com.netgrif.application.engine.migration.helpers.PetriNetMigrationHelper +import com.netgrif.application.engine.migration.helpers.TaskMigrationHelper import com.netgrif.application.engine.petrinet.domain.I18nString import com.netgrif.application.engine.petrinet.domain.PetriNet import com.netgrif.application.engine.petrinet.domain.Transition @@ -42,6 +46,15 @@ import java.util.stream.Collectors @Component class MigrationHelper { + @Autowired + private CaseMigrationHelper caseMigrationHelper + + @Autowired + private TaskMigrationHelper taskMigrationHelper + + @Autowired + private PetriNetMigrationHelper petriNetMigrationHelper + @Autowired private CaseRepository caseRepository @@ -91,14 +104,17 @@ class MigrationHelper { return importerProvider.get() } + Closure<PetriNet> updateRoleEvents = { PetriNet existing, PetriNet reimported -> + petriNetMigrationHelper.updateRoleEvents(existing, reimported) + } + /** * Updates all cases filtered by filter Predicate. Update closure is called on each filtered case. * @param update Instance of Closure, which should contain code that will be executed for every Case matched by filter * @param filter Instance of Predicate, to filter which cases should be updated */ void updateCases(Closure update, Predicate filter) { - log.info("Updating cases with filter ${filter.toString()} and update ${update.toString()}") - iterateCases(update, { Page<Case> cases -> caseRepository.saveAll(cases) }, filter) + caseMigrationHelper.updateCases(update, filter) } /** @@ -107,26 +123,8 @@ class MigrationHelper { * @param sleepFor Optional attribute to set sleep time (in milliseconds) to sleep for after each iterated page. Default 0ms * @param filter Instance of Predicate, to filter which cases should be iterated */ - void iterateCases(Closure update, Closure pageProcessed = {}, long sleepFor = 0, Predicate filter) { - long caseCount = caseRepository.count(filter) - long numOfPages = ((caseCount / 100.0) + 1) as long - log.info("Processing cases with filter ${filter.toString()}: $numOfPages pages") - numOfPages.times { page -> - log.info("Page $page / $numOfPages") - - Page<Case> cases = caseRepository.findAll(filter, PageRequest.of(page, 100)) - - cases.each { - log.debug("Processing case with id ${it.stringId}") - log.trace("Processing case ${it.toString()}") - update(it) - } - pageProcessed(cases) - if (sleepFor != 0) { - log.debug("Pausing migration for ${sleepFor} milliseconds") - sleep(sleepFor) - } - } + void iterateCases(Closure update, Closure pageProcessed = AbstractMigrationHelper.DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { + caseMigrationHelper.iterateCases(update, pageProcessed, sleepFor, filter) } /** @@ -136,37 +134,7 @@ class MigrationHelper { * @param pageSize Optional attribute to set page size. Default page size 100.0 */ void updateCasesCursor(Closure update, String processIdentifier, double pageSize = 100.0) { - long caseCount = caseRepository.count(QCase.case$.processIdentifier.eq(processIdentifier)) - long numOfPages = ((caseCount / pageSize) + 1) as long - log.info("Migrating process $processIdentifier") - log.info("Page size: $pageSize") - log.info("Processing cases: $numOfPages pages") - ObjectId lastId = null - if (caseCount > 0) { - for (int p = 0; p < numOfPages; p++) { - try { - log.info("Page " + (p + 1) + " / $numOfPages") - - Query query = new Query() - if (lastId == null) { - query.skip(0) - } else { - query.addCriteria(Criteria.where("_id").gt(lastId)) - } - query.addCriteria(Criteria.where("processIdentifier").is(processIdentifier)) - query.limit(pageSize as Integer) - - List<Case> cases = mongoTemplate.find(query, Case.class) - cases.each { update(it) } - cases = caseRepository.saveAll(cases) - - lastId = cases.get(cases.size() - 1).get_id() - } catch (Exception e) { - log.error("Failed to iterate page " + (p + 1), e.getMessage()) - break - } - } - } + caseMigrationHelper.updateCasesCursor(update, processIdentifier, pageSize) } /** @@ -175,35 +143,7 @@ class MigrationHelper { * @param pageSize Optional attribute to set page size. Default page size 100.0 */ void updateAllCasesCursor(Closure update, double pageSize = 100.0) { - long caseCount = caseRepository.count() - long numOfPages = ((caseCount / pageSize) + 1) as long - log.info("Page size: $pageSize") - log.info("Processing cases: $numOfPages pages") - ObjectId lastId = null - if (caseCount > 0) { - for (int p = 0; p < numOfPages; p++) { - try { - log.info("Page " + (p + 1) + " / $numOfPages") - - Query query = new Query() - if (lastId == null) { - query.skip(0) - } else { - query.addCriteria(Criteria.where("_id").gt(lastId)) - } - query.limit(pageSize as Integer) - - List<Case> cases = mongoTemplate.find(query, Case.class) - cases.each { update(it) } - cases = caseRepository.saveAll(cases) - - lastId = cases.get(cases.size() - 1).get_id() - } catch (ArrayIndexOutOfBoundsException e) { - log.error("Failed to iterate page " + (p + 1)) - break - } - } - } + caseMigrationHelper.updateAllCasesCursor(update, pageSize) } /** @@ -212,8 +152,7 @@ class MigrationHelper { * @param filter Instance of Predicate, to filter which tasks should be updated */ void updateTasks(Closure update, Predicate filter) { - log.info("Updating tasks with filter ${filter.toString()} and update ${update.toString()}") - iterateTasks(update, { Page<Task> tasks -> taskRepository.saveAll(tasks) }, filter) + taskMigrationHelper.updateTasks(update, filter) } /** @@ -222,21 +161,8 @@ class MigrationHelper { * @param sleepFor Optional attribute to set sleep time (in milliseconds) to sleep for after each iterated page. Default 0ms * @param filter Instance of Predicate, to filter which tasks should be iterated */ - void iterateTasks(Closure update, Closure pageProcessed = {}, long sleepFor = 0, Predicate filter) { - long taskCount = taskRepository.count(filter) - long numOfPages = ((taskCount / 100.0) + 1) as long - log.info("Processing tasks with filter ${filter.toString()}: $numOfPages pages") - numOfPages.times { page -> - log.info("Page $page / $numOfPages") - - Page<Task> tasks = taskRepository.findAll(filter, PageRequest.of(page, 100)) - - tasks.each { update(it) } - pageProcessed(tasks) - if (sleepFor != 0) { - sleep(sleepFor) - } - } + void iterateTasks(Closure update, Closure pageProcessed = AbstractMigrationHelper.DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { + taskMigrationHelper.iterateTasks(update, pageProcessed, sleepFor, filter) } /** @@ -246,38 +172,7 @@ class MigrationHelper { * @param pageSize Optional attribute to set page size. Default page size 100.0 */ void updateTasksCursor(Closure update, String processIdentifier, double pageSize = 100.0) { - String processId = petriNetService.getNewestVersionByIdentifier(processIdentifier).stringId - long taskCount = taskRepository.count(QTask.task.processId.eq(processId)) - long numOfPages = ((taskCount / pageSize) + 1) as long - log.info("Migrating process $processIdentifier") - log.info("Page size: $pageSize") - log.info("Processing tasks: $numOfPages pages") - ObjectId lastId = null - if (taskCount > 0) { - for (int p = 0; p < numOfPages; p++) { - try { - log.info("Page " + (p + 1) + " / $numOfPages") - - Query query = new Query() - if (lastId == null) { - query.skip(0) - } else { - query.addCriteria(Criteria.where("_id").gt(lastId)) - } - query.addCriteria(Criteria.where("processId").is(processId)) - query.limit(pageSize as Integer) - - List<Task> tasks = mongoTemplate.find(query, Task.class) - tasks.each { update(it) } - tasks = taskRepository.saveAll(tasks) - - lastId = tasks.get(tasks.size() - 1).objectId - } catch (ArrayIndexOutOfBoundsException e) { - log.error("Failed to iterate page " + (p + 1)) - break - } - } - } + taskMigrationHelper.updateTasksCursor(update, processIdentifier, pageSize) } /** @@ -288,39 +183,7 @@ class MigrationHelper { * @param pageSize Optional attribute to set page size. Default page size 100.0 */ void updateSpecificTasksCursor(Closure update, String processIdentifier, List<String> transitionIds, double pageSize = 100.0) { - String processId = petriNetService.getNewestVersionByIdentifier(processIdentifier).stringId - long taskCount = taskRepository.count(QTask.task.processId.eq(processId) & QTask.task.transitionId.in(transitionIds)) - long numOfPages = ((taskCount / pageSize) + 1) as long - log.info("Migrating process $processIdentifier transitions ${transitionIds.toString()}") - log.info("Page size: $pageSize") - log.info("Processing tasks: $numOfPages pages") - ObjectId lastId = null - if (taskCount > 0) { - for (int p = 0; p < numOfPages; p++) { - try { - log.info("Page " + (p + 1) + " / $numOfPages") - - Query query = new Query() - if (lastId == null) { - query.skip(0) - } else { - query.addCriteria(Criteria.where("_id").gt(lastId)) - } - query.addCriteria(Criteria.where("processId").is(processId)) - query.addCriteria(Criteria.where("transitionId").in(transitionIds)) - query.limit(pageSize as Integer) - - List<Task> tasks = mongoTemplate.find(query, Task.class) - tasks.each { update(it) } - tasks = taskRepository.saveAll(tasks) - - lastId = tasks.get(tasks.size() - 1).objectId - } catch (ArrayIndexOutOfBoundsException e) { - log.error("Failed to iterate page " + (p + 1)) - break - } - } - } + taskMigrationHelper.updateSpecificTasksCursor(update, processIdentifier, transitionIds, pageSize) } /** @@ -329,35 +192,7 @@ class MigrationHelper { * @param pageSize Optional attribute to set page size. Default page size 100.0 */ void updateAllTasksCursor(Closure update, double pageSize = 100.0) { - long taskCount = taskRepository.count() - long numOfPages = ((taskCount / pageSize) + 1) as long - log.info("Page size: $pageSize") - log.info("Processing tasks: $numOfPages pages") - ObjectId lastId = null - if (taskCount > 0) { - for (int p = 0; p < numOfPages; p++) { - try { - log.info("Page " + (p + 1) + " / $numOfPages") - - Query query = new Query() - if (lastId == null) { - query.skip(0) - } else { - query.addCriteria(Criteria.where("_id").gt(lastId)) - } - query.limit(pageSize as Integer) - - List<Task> tasks = mongoTemplate.find(query, Task.class) - tasks.each { update(it) } - tasks = taskRepository.saveAll(tasks) - - lastId = tasks.get(tasks.size() - 1).objectId - } catch (ArrayIndexOutOfBoundsException e) { - log.error("Failed to iterate page " + (p + 1)) - break - } - } - } + taskMigrationHelper.updateAllTasksCursor(update, pageSize) } /** @@ -366,8 +201,7 @@ class MigrationHelper { * @param resource Resource object with new version of Petri Net model */ void updateNetIgnoreRoles(String identifier, Resource resource, List<Closure<PetriNet>> customUpdates = null) { - PetriNet reimported = service.importPetriNet(resource.inputStream, VersionType.MAJOR, userService.system.transformToLoggedUser()).getNet() - updateNetIgnoreRoles(service.getNewestVersionByIdentifier(identifier), reimported, customUpdates) + petriNetMigrationHelper.updateNetIgnoreRoles(identifier, resource, customUpdates) } /** @@ -376,12 +210,7 @@ class MigrationHelper { * @param fileName File name of new version of Petri Net model */ void updateNetIgnoreRoles(String identifier, String fileName, List<Closure<PetriNet>> customUpdates = null) { - PetriNet currentNet = service.getNewestVersionByIdentifier(identifier) - InputStream inputStream = new ClassPathResource("petriNets/$fileName" as String).inputStream - ByteArrayOutputStream outputStream = new ByteArrayOutputStream() - IOUtils.copy(inputStream, outputStream) - PetriNet reimported = getImporter().importPetriNet(new ByteArrayInputStream(outputStream.toByteArray())).get() - updateNetIgnoreRoles(currentNet, reimported, customUpdates) + petriNetMigrationHelper.updateNetIgnoreRoles(identifier, fileName, customUpdates) } /** @@ -390,84 +219,7 @@ class MigrationHelper { * @param reimported New version of Petri Net object, its values will be applied to currentNet */ void updateNetIgnoreRoles(PetriNet currentNet, PetriNet reimported, List<Closure<PetriNet>> customUpdates) { - if (!currentNet) { - log.warn("Net $reimported.identifier does not exist") - return - } - Map<String, ProcessRole> oldProcessRoles = currentNet.roles - Map<String, ProcessRole> newProcessRoles = reimported.roles - - reimported = replaceUserFieldRoleReferences(currentNet, reimported) - - ProcessRole defaultRole = roleRepository.findAllByName_DefaultValue(ProcessRole.DEFAULT_ROLE).first() - ProcessRole anonymousRole = roleRepository.findAllByName_DefaultValue(ProcessRole.ANONYMOUS_ROLE).first() - - currentNet.places = reimported.places - currentNet.transitions = reimported.transitions - currentNet.arcs = reimported.arcs - currentNet.dataSet = reimported.clone().dataSet - currentNet.transactions = reimported.transactions - currentNet.importId = reimported.importId - currentNet.caseEvents = reimported.caseEvents - currentNet.processEvents = reimported.processEvents - currentNet.negativeViewRoles = reimported.negativeViewRoles - currentNet.userRefs = reimported.userRefs - currentNet.functions = reimported.functions - - def newPermissions = [:] - reimported.permissions.each { id, permissions -> - def newRole = newProcessRoles[id] - - if (!newRole && (defaultRole.stringId == id || anonymousRole.stringId == id)) { - log.info("Default role $id on process $currentNet.identifier detected, skipping") - newPermissions[id] = permissions - - } else { - def oldRole = oldProcessRoles.values().find { - it.importId == newRole.importId - } - - if (!oldRole) { - log.warn("Old role does not exist for role $newRole.importId") - return - } - newPermissions[oldRole.stringId] = permissions - } - } - currentNet.permissions = newPermissions as Map<String, Map<String, Boolean>> - - currentNet.transitions.each { id, t -> - Map<String, Map<String, Boolean>> oldRoles = new HashMap<>() - t.roles.each { roleMongoId, permissions -> - def newRole = newProcessRoles[roleMongoId] - - if (!newRole && (defaultRole.stringId == roleMongoId || anonymousRole.stringId == roleMongoId)) { - log.info("Default role $roleMongoId on transition ${t.importId} detected, skipping") - oldRoles[roleMongoId] = permissions - - } else { - def oldRole = oldProcessRoles.values().find { - it.importId == newRole.importId - } - - if (!oldRole) { - log.warn("Old role does not exist for role $newRole.importId") - return - } - oldRoles[oldRole.stringId] = permissions - } - } - t.roles = oldRoles - } - - resolveDataOrder(currentNet) - - customUpdates && customUpdates.each { Closure<PetriNet> customUpdate -> - currentNet = customUpdate(currentNet, reimported) - } - - service.save(currentNet) - log.info("Migrated $currentNet.identifier") + petriNetMigrationHelper.updateNetIgnoreRoles(currentNet, reimported, customUpdates) } /** @@ -478,8 +230,7 @@ class MigrationHelper { * @param permissions New role permissions on transition */ void updateTransitionRoles(PetriNet net, String transitionId, ProcessRole role, Map<String, Boolean> permissions) { - Transition trans = net.transitions.values().find { it.importId == transitionId } - trans.roles[role.stringId] = permissions + petriNetMigrationHelper.updateTransitionRoles(net, transitionId, role, permissions) } /** @@ -490,8 +241,7 @@ class MigrationHelper { * @param permissions New role permissions on transition */ void updateTransitionRoles(PetriNet net, String transitionId, String roleImportId, Map<String, Boolean> permissions) { - ProcessRole role = net.roles.values().find { it.importId == roleImportId } - updateTransitionRoles(net, transitionId, role, permissions) + petriNetMigrationHelper.updateTransitionRoles(net, transitionId, roleImportId, permissions) } /** @@ -501,10 +251,7 @@ class MigrationHelper { * @param permissions New role permissions on transition */ Closure<PetriNet> updateTransitionRolesClosure(String transitionId, String roleImportId, Map<String, Boolean> permissions) { - return { PetriNet petriNet, PetriNet reimported -> - updateTransitionRoles(petriNet, transitionId, roleImportId, permissions) - return petriNet - } + petriNetMigrationHelper.updateTransitionRolesClosure(transitionId, roleImportId, permissions) } /** @@ -513,59 +260,7 @@ class MigrationHelper { * @param fileName File name of new version of Petri Net model */ void updateDataSet(String identifier, String fileName, Closure<PetriNet> customUpdate = null) { - PetriNet existing = service.getNewestVersionByIdentifier(identifier) - PetriNet reimported = getImporter().importPetriNet(new File("src/main/resources/petriNets/" + fileName)).get() - - reimported = replaceUserFieldRoleReferences(existing, reimported) - - existing.dataSet = reimported.dataSet - - if (customUpdate) { - existing = customUpdate(existing, reimported) - } - - service.save(existing) - log.info("Migrated $identifier") - } - - /** - * Updates roles of USER fields in existing Petri Net model, WARNING: new roles referenced in USER fields will be ignored! They need to be migrated manually - * @param originalNet Current Petri Net object that will be updated - * @param reimportedNet New version of Petri Net object, its values will be applied to currentNet - */ - private PetriNet replaceUserFieldRoleReferences(PetriNet originalNet, PetriNet reimportedNet) { - Map<String, ProcessRole> originalNetRoles = [:] // importId: processRole - originalNet.roles.forEach { name, role -> - originalNetRoles.put(role.importId, role) - } - - reimportedNet.dataSet.entrySet().stream().filter { - it.value.type == FieldType.USER - - }.forEach { entry -> - UserField field = (reimportedNet.dataSet[entry.key] as UserField) - field.roles = field.roles.collect { roleId -> - Optional<ProcessRole> roleOpt = Optional.ofNullable(reimportedNet.roles[roleId]) - if (roleOpt.isPresent()) { - ProcessRole oldRole = originalNetRoles[roleOpt.get().importId] - - if (!oldRole) { - log.warn("Process role in process ${originalNet.identifier} ${originalNet.stringId} with import id ${roleOpt.get().importId} not found!") - return null - - } else { - return oldRole.stringId - } - - } else { - log.warn("Role not found! ${roleId}") - return null - } - }.stream().filter { Objects.nonNull(it) }.collect() - - } - - return reimportedNet + petriNetMigrationHelper.updateDataSet(identifier, fileName, customUpdate) } /** @@ -575,7 +270,7 @@ class MigrationHelper { * @param title Title of the new Process Role */ def createRoleInNet(String identifier, String id, String title, Map<EventType, Event> events = [:]) { - return createRoleInNet(identifier, id, new I18nString(title), events) + return petriNetMigrationHelper.createRoleInNet(identifier, id, title, events) } /** @@ -585,18 +280,7 @@ class MigrationHelper { * @param title Title of the new Process Role */ def createRoleInNet(String identifier, String id, I18nString title, Map<EventType, Event> events = [:]) { - PetriNet net = service.getNewestVersionByIdentifier(identifier) - - ProcessRole role = new ProcessRole() - role.setImportId(id) - role.setName(title) - role.setEvents(events) - - role = roleRepository.save(role) - net.addRole(role) - netRepository.save(net) - - return role + return petriNetMigrationHelper.createRoleInNet(identifier, id, title, events) } /** @@ -605,7 +289,7 @@ class MigrationHelper { * @param title Title of the new Process Role */ def createGlobalRole(String id, String title, Map<EventType, Event> events = [:]) { - return createGlobalRole(id, new I18nString(title), events) + return petriNetMigrationHelper.createGlobalRole(id, title, event) } /** @@ -614,36 +298,7 @@ class MigrationHelper { * @param title Title of the new Process Role */ def createGlobalRole(String id, I18nString title, Map<EventType, Event> events = [:]) { - ProcessRole role = new ProcessRole() - - if (!id.startsWith("global_")) { - role.setImportId("global_" + id) - } else { - role.setImportId(id) - } - role.setName(title) - role.setEvents(events) - role.setGlobal(true) - - role = roleRepository.save(role) - - return role - } - - /** - * Replaces events in roles from existing with events from roles from reimported - */ - Closure<PetriNet> updateRoleEvents = { PetriNet existing, PetriNet reimported -> - List<ProcessRole> newRoles = reimported.roles.values() as List - List<ProcessRole> oldRoles = existing.roles.values() as List - - newRoles.each { newRole -> - ProcessRole role = oldRoles.find { it.importId == newRole.importId } - role.events = newRole.events - roleRepository.save(role) - } - - return existing + return petriNetMigrationHelper.createGlobalRole(id, title, events) } /** @@ -653,8 +308,7 @@ class MigrationHelper { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ void reloadTasks(Case useCase, PetriNet net) { - setPetriNet(useCase, net) - taskService.reloadTasks(useCase) + taskMigrationHelper.reloadTasks(useCase, net) } /** @@ -663,19 +317,7 @@ class MigrationHelper { * @param useCase Instance of Case that will be indexed into elasticsearch index */ void elasticIndex(Case useCase) { - try { - setPetriNet(useCase, service.getNewestVersionByIdentifier(useCase.processIdentifier)) - assert useCase.petriNet - elasticCaseService.indexNow(caseMappingService.transform(useCase)) - } catch (Exception ex) { - if (useCase.lastModified == null) { - log.error("Creating new lastModified date for $useCase.stringId") - useCase.lastModified = LocalDateTime.now() - elasticCaseService.indexNow(caseMappingService.transform(useCase)) - } else { - log.error("Failed to index $useCase.stringId", ex) - } - } + caseMigrationHelper.elasticIndex(useCase) } /** @@ -683,11 +325,7 @@ class MigrationHelper { * @param task Instance of Task that will be indexed into elasticsearch index */ void elasticTaskIndex(Task task) { - try { - elasticTaskService.indexNow(elasticTaskMappingService.transform(task)) - } catch (Exception e) { - log.error("Failed to index $task.stringId", e) - } + taskMigrationHelper.elasticTaskIndex(task) } /** @@ -698,10 +336,7 @@ class MigrationHelper { * @param permissions Map of permissions for the role */ void addRoleToExistingTasks(ProcessRole role, PetriNet net, List<String> transitionIds, Map<String, Boolean> permissions) { - updateTasks({ Task task -> - log.info("Add role '${role.getName()}' with roleId=${role.getImportId()} to transitionId=${task.getTransitionId()} in task ${task.stringId}") - task.addRole(role.getStringId(), permissions) - }, QTask.task.transitionId.in(transitionIds) & QTask.task.processId.eq(net.getStringId())) + taskMigrationHelper.addRoleToExistingTasks(role, net, transitionIds, permissions) } /** @@ -710,10 +345,7 @@ class MigrationHelper { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ void setPetriNet(Case useCase, PetriNet net) { - PetriNet model = net.clone() - model.initializeTokens(useCase.getActivePlaces()) - model.initializeArcs(useCase.getDataSet()) - useCase.setPetriNet(model) + PetriNetMigrationHelper.setPetriNet(useCase, net) } /** @@ -722,9 +354,7 @@ class MigrationHelper { * @param toDelete List of field IDs that will be deleted from useCase */ void deleteDataFields(Case useCase, List<String> toDelete) { - toDelete.each { dataFieldID -> - useCase.dataSet.remove(dataFieldID) - } + caseMigrationHelper.deleteDataFields(useCase, toDelete) } /** @@ -733,12 +363,7 @@ class MigrationHelper { * @param toChange List of field IDs for value change */ void changeDataFieldsValueFromNumberToText(Case useCase, List<String> toChange) { - toChange.each { dataFieldID -> - if (useCase.dataSet[dataFieldID].value && (useCase.dataSet[dataFieldID].value != null || useCase.dataSet[dataFieldID].value != "")) { - double value = useCase.dataSet[dataFieldID].value as double - useCase.dataSet[dataFieldID].value = value as String - } - } + caseMigrationHelper.changeDataFieldsValueFromNumberToText(useCase, toChange) } /** @@ -747,16 +372,7 @@ class MigrationHelper { * @param toChange List of field IDs for value change */ void changeDataFieldsValueFromTextToNumber(Case useCase, List<String> toChange) { - toChange.each { dataFieldID -> - if (useCase.dataSet[dataFieldID].value && useCase.dataSet[dataFieldID].value != "") { - try { - useCase.dataSet[dataFieldID].value = useCase.dataSet[dataFieldID].value as double - } catch (Exception e) { - useCase.dataSet[dataFieldID].value = null - log.error("[${useCase.stringId}] could not convert value ${useCase.dataSet[dataFieldID].value} in field ${dataFieldID}", e) - } - } - } + caseMigrationHelper.changeDataFieldsValueFromTextToNumber(useCase, toChange) } /** @@ -765,9 +381,7 @@ class MigrationHelper { * @param toAdd Map<field id, init value of field> */ void addTextDataFields(Case useCase, Map<String, String> toAdd) { - toAdd.each { dataFieldID, value -> - useCase.dataSet[dataFieldID] = new DataField(value) - } + caseMigrationHelper.addTextDataFields(useCase, toAdd) } /** @@ -776,20 +390,7 @@ class MigrationHelper { * @param toChange List of field IDs for value change */ void changeDataFieldsValueFromEnumerationToMultichoice(Case useCase, List<String> toChange) { - toChange.each { dataFieldID -> - if (useCase.dataSet[dataFieldID].value && useCase.dataSet[dataFieldID].value != null) { - def value - if (useCase.dataSet[dataFieldID].value instanceof I18nString) { - value = useCase.dataSet[dataFieldID].value as I18nString - } else { - value = new I18nString(useCase.dataSet[dataFieldID].value as String) - } - - def newSet = new HashSet<I18nString>() - newSet.add(value) - useCase.dataSet[dataFieldID].value = newSet - } - } + caseMigrationHelper.changeDataFieldsValueFromEnumerationToMultichoice(useCase, toChange) } /** @@ -798,15 +399,7 @@ class MigrationHelper { * @param toAdd Map<field id, list of choices to add into data data field> */ void addChoices(Case useCase, Map<String, List<String>> toAdd) { - toAdd.each { dataFieldID, newChoices -> - if (useCase.dataSet[dataFieldID].choices == null) { - useCase.dataSet[dataFieldID].setChoices(new HashSet<I18nString>()) - } - - newChoices.each { - useCase.dataSet[dataFieldID].choices.add(new I18nString(it)) - } - } + caseMigrationHelper.addChoices(useCase, toAdd) } /** @@ -815,15 +408,7 @@ class MigrationHelper { * @param toAdd Map<field id, list of choices to add into data field> */ void removeChoices(Case useCase, Map<String, List<String>> toRemove) { - toRemove.each { dataFieldID, choicesToRemove -> - if (useCase.dataSet[dataFieldID].value != null) { - (useCase.dataSet[dataFieldID].value as Set).removeAll(choicesToRemove) - } - - if (useCase.dataSet[dataFieldID].choices != null) { - useCase.dataSet[dataFieldID].choices.removeAll(choicesToRemove) - } - } + caseMigrationHelper.removeChoices(useCase, toRemove) } /** diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index 54dc939930e..252d137cbda 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -1,8 +1,14 @@ package com.netgrif.application.engine.migration.helpers +import com.netgrif.application.engine.elastic.service.ElasticCaseMappingService +import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseMappingService +import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.CaseMigrationProperties +import com.netgrif.application.engine.petrinet.domain.I18nString +import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService import com.netgrif.application.engine.workflow.domain.Case +import com.netgrif.application.engine.workflow.domain.DataField import com.querydsl.core.types.Predicate import groovy.util.logging.Slf4j import org.springframework.data.mongodb.core.BulkOperations @@ -11,6 +17,8 @@ import org.springframework.data.mongodb.core.query.Criteria import org.springframework.data.mongodb.core.query.Query import org.springframework.stereotype.Component +import java.time.LocalDateTime + /** * Helper class for managing migrations of Case objects in the application. * Provides methods for updating and iterating over case objects, filtered @@ -28,6 +36,12 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { */ private CaseMigrationProperties caseMigrationProperties + private IPetriNetService petriNetService + + private IElasticCaseService elasticCaseService + + private IElasticCaseMappingService elasticCaseMappingService + /** * Constructs a CaseMigrationHelper instance with * the provided MongoTemplate and migration configuration properties. @@ -36,9 +50,15 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param migrationConfigurationProperties Properties for migration configuration, including cases. */ CaseMigrationHelper(MongoTemplate mongoTemplate, - MigrationConfigurationProperties migrationConfigurationProperties) { + MigrationConfigurationProperties migrationConfigurationProperties, + IPetriNetService petriNetService, + IElasticCaseService elasticCaseService, + IElasticCaseMappingService elasticCaseMappingService) { super(Case.class, mongoTemplate) this.caseMigrationProperties = migrationConfigurationProperties.cases + this.petriNetService = petriNetService + this.elasticCaseService = elasticCaseService + this.elasticCaseMappingService = elasticCaseMappingService } /** @@ -113,5 +133,136 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { void updateAllCasesCursor(Closure update, double pageSize = 100.0) { iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int) } + + /** + * Indexes provided case in elasticsearch + * handles useCase.petriNet internally + * @param useCase Instance of Case that will be indexed into elasticsearch index + */ + void elasticIndex(Case useCase) { + try { + PetriNetMigrationHelper.setPetriNet(useCase, petriNetService.getNewestVersionByIdentifier(useCase.processIdentifier)) + assert useCase.petriNet + elasticCaseService.indexNow(elasticCaseMappingService.transform(useCase)) + } catch (Exception ex) { + if (useCase.lastModified == null) { + log.error("Creating new lastModified date for $useCase.stringId") + useCase.lastModified = LocalDateTime.now() + elasticCaseService.indexNow(elasticCaseMappingService.transform(useCase)) + } else { + log.error("Failed to index $useCase.stringId", ex) + } + } + } + + /** + * Delete given data fields from useCase + * @param useCase Instance of Case + * @param toDelete List of field IDs that will be deleted from useCase + */ + void deleteDataFields(Case useCase, List<String> toDelete) { + toDelete.each { dataFieldID -> + useCase.dataSet.remove(dataFieldID) + } + } + + /** + * Changes value of given data fields from number to text + * @param useCase Instance of Case + * @param toChange List of field IDs for value change + */ + void changeDataFieldsValueFromNumberToText(Case useCase, List<String> toChange) { + toChange.each { dataFieldID -> + if (useCase.dataSet[dataFieldID].value && (useCase.dataSet[dataFieldID].value != null || useCase.dataSet[dataFieldID].value != "")) { + double value = useCase.dataSet[dataFieldID].value as double + useCase.dataSet[dataFieldID].value = value as String + } + } + } + + /** + * Changes value of given data fields from text to number + * @param useCase Instance of Case + * @param toChange List of field IDs for value change + */ + void changeDataFieldsValueFromTextToNumber(Case useCase, List<String> toChange) { + toChange.each { dataFieldID -> + if (useCase.dataSet[dataFieldID].value && useCase.dataSet[dataFieldID].value != "") { + try { + useCase.dataSet[dataFieldID].value = useCase.dataSet[dataFieldID].value as double + } catch (Exception e) { + useCase.dataSet[dataFieldID].value = null + log.error("[${useCase.stringId}] could not convert value ${useCase.dataSet[dataFieldID].value} in field ${dataFieldID}", e) + } + } + } + } + + /** + * Adds new data fields with their init value into useCase + * @param useCase Instance of Case + * @param toAdd Map<field id, init value of field> + */ + void addTextDataFields(Case useCase, Map<String, String> toAdd) { + toAdd.each { dataFieldID, value -> + useCase.dataSet[dataFieldID] = new DataField(value) + } + } + + /** + * Changes value of given data fields from enumeration to multichoice + * @param useCase Instance of Case + * @param toChange List of field IDs for value change + */ + void changeDataFieldsValueFromEnumerationToMultichoice(Case useCase, List<String> toChange) { + toChange.each { dataFieldID -> + if (useCase.dataSet[dataFieldID].value && useCase.dataSet[dataFieldID].value != null) { + def value + if (useCase.dataSet[dataFieldID].value instanceof I18nString) { + value = useCase.dataSet[dataFieldID].value as I18nString + } else { + value = new I18nString(useCase.dataSet[dataFieldID].value as String) + } + + def newSet = new HashSet<I18nString>() + newSet.add(value) + useCase.dataSet[dataFieldID].value = newSet + } + } + } + + /** + * Adds new choices into enumeration or multichoice field + * @param useCase Instance of Case + * @param toAdd Map<field id, list of choices to add into data data field> + */ + void addChoices(Case useCase, Map<String, List<String>> toAdd) { + toAdd.each { dataFieldID, newChoices -> + if (useCase.dataSet[dataFieldID].choices == null) { + useCase.dataSet[dataFieldID].setChoices(new HashSet<I18nString>()) + } + + newChoices.each { + useCase.dataSet[dataFieldID].choices.add(new I18nString(it)) + } + } + } + + /** + * Removes choices from enumeration or multichoice field + * @param useCase Instance of Case + * @param toAdd Map<field id, list of choices to add into data field> + */ + void removeChoices(Case useCase, Map<String, List<String>> toRemove) { + toRemove.each { dataFieldID, choicesToRemove -> + if (useCase.dataSet[dataFieldID].value != null) { + (useCase.dataSet[dataFieldID].value as Set).removeAll(choicesToRemove) + } + + if (useCase.dataSet[dataFieldID].choices != null) { + useCase.dataSet[dataFieldID].choices.removeAll(choicesToRemove) + } + } + } } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy index 1bbf23f8bb1..3706db19465 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy @@ -1,31 +1,63 @@ package com.netgrif.application.engine.migration.helpers -import com.netgrif.application.engine.AsyncRunner + +import com.netgrif.application.engine.importer.service.Importer import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.PetriNetMigrationProperties +import com.netgrif.application.engine.petrinet.domain.I18nString import com.netgrif.application.engine.petrinet.domain.PetriNet +import com.netgrif.application.engine.petrinet.domain.Transition +import com.netgrif.application.engine.petrinet.domain.VersionType +import com.netgrif.application.engine.petrinet.domain.dataset.Field +import com.netgrif.application.engine.petrinet.domain.dataset.FieldType +import com.netgrif.application.engine.petrinet.domain.dataset.UserField +import com.netgrif.application.engine.petrinet.domain.events.Event +import com.netgrif.application.engine.petrinet.domain.events.EventType +import com.netgrif.application.engine.petrinet.domain.roles.ProcessRole +import com.netgrif.application.engine.petrinet.domain.roles.ProcessRoleRepository +import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService +import com.netgrif.application.engine.workflow.domain.Case import groovy.util.logging.Slf4j +import org.apache.tomcat.util.http.fileupload.IOUtils +import org.springframework.core.io.ClassPathResource +import org.springframework.core.io.Resource import org.springframework.data.mongodb.core.BulkOperations import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Criteria import org.springframework.data.mongodb.core.query.Query import org.springframework.stereotype.Component +import javax.inject.Provider +import java.text.Collator +import java.util.stream.Collectors + @Slf4j @Component class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { private PetriNetMigrationProperties petriNetMigrationProperties + private IPetriNetService petriNetService + + private ProcessRoleRepository processRoleRepository + + private Provider<Importer> importerProvider + /** * Constructs a new PetriNetMigrationHelper with the specified MongoTemplate. * * @param mongoTemplate the {@link MongoTemplate} to use for interacting with MongoDB */ PetriNetMigrationHelper(MongoTemplate mongoTemplate, - MigrationConfigurationProperties migrationConfigurationProperties) { + MigrationConfigurationProperties migrationConfigurationProperties, + IPetriNetService petriNetService, + ProcessRoleRepository processRoleRepository, + Provider<Importer> importerProvider) { super(PetriNet.class, mongoTemplate) this.petriNetMigrationProperties = migrationConfigurationProperties.petriNets + this.petriNetService = petriNetService + this.processRoleRepository = processRoleRepository + this.importerProvider = importerProvider } @Override @@ -41,5 +73,323 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { bulkOperations.replaceOne(Query.query(Criteria.where("_id").is(document.getObjectId())), document) } + /** + * Updates existing Petri Net model with new values. New process roles are ignored! New roles in existing user type fields will be ignored! + * @param identifier Identifier of Petri Net model that is being updated + * @param resource Resource object with new version of Petri Net model + */ + void updateNetIgnoreRoles(String identifier, Resource resource, List<Closure<PetriNet>> customUpdates = null) { + PetriNet reimported = petriNetService.importPetriNet(resource.inputStream, VersionType.MAJOR, userService.system.transformToLoggedUser()).getNet() + updateNetIgnoreRoles(petriNetService.getNewestVersionByIdentifier(identifier), reimported, customUpdates) + } + + /** + * Updates existing Petri Net model with new values. New process roles are ignored! New roles in existing user type fields will be ignored! + * @param identifier Identifier of Petri Net model that is being updated + * @param fileName File name of new version of Petri Net model + */ + void updateNetIgnoreRoles(String identifier, String fileName, List<Closure<PetriNet>> customUpdates = null) { + PetriNet currentNet = petriNetService.getNewestVersionByIdentifier(identifier) + InputStream inputStream = new ClassPathResource("petriNets/$fileName" as String).inputStream + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + IOUtils.copy(inputStream, outputStream) + PetriNet reimported = getImporter().importPetriNet(new ByteArrayInputStream(outputStream.toByteArray())).get() + updateNetIgnoreRoles(currentNet, reimported, customUpdates) + } + + /** + * Updates existing Petri Net model with new values. New process roles are ignored! New roles in existing user type fields will be ignored! + * @param currentNet Current Petri Net object that will be updated + * @param reimported New version of Petri Net object, its values will be applied to currentNet + */ + void updateNetIgnoreRoles(PetriNet currentNet, PetriNet reimported, List<Closure<PetriNet>> customUpdates) { + if (!currentNet) { + log.warn("Net $reimported.identifier does not exist") + return + } + Map<String, ProcessRole> oldProcessRoles = currentNet.roles + Map<String, ProcessRole> newProcessRoles = reimported.roles + + reimported = replaceUserFieldRoleReferences(currentNet, reimported) + + ProcessRole defaultRole = processRoleRepository.findAllByName_DefaultValue(ProcessRole.DEFAULT_ROLE).first() + ProcessRole anonymousRole = processRoleRepository.findAllByName_DefaultValue(ProcessRole.ANONYMOUS_ROLE).first() + + currentNet.places = reimported.places + currentNet.transitions = reimported.transitions + currentNet.arcs = reimported.arcs + currentNet.dataSet = reimported.clone().dataSet + currentNet.transactions = reimported.transactions + currentNet.importId = reimported.importId + currentNet.caseEvents = reimported.caseEvents + currentNet.processEvents = reimported.processEvents + currentNet.negativeViewRoles = reimported.negativeViewRoles + currentNet.userRefs = reimported.userRefs + currentNet.functions = reimported.functions + + def newPermissions = [:] + reimported.permissions.each { id, permissions -> + def newRole = newProcessRoles[id] + + if (!newRole && (defaultRole.stringId == id || anonymousRole.stringId == id)) { + log.info("Default role $id on process $currentNet.identifier detected, skipping") + newPermissions[id] = permissions + + } else { + def oldRole = oldProcessRoles.values().find { + it.importId == newRole.importId + } + + if (!oldRole) { + log.warn("Old role does not exist for role $newRole.importId") + return + } + newPermissions[oldRole.stringId] = permissions + } + } + currentNet.permissions = newPermissions as Map<String, Map<String, Boolean>> + + currentNet.transitions.each { id, t -> + Map<String, Map<String, Boolean>> oldRoles = new HashMap<>() + t.roles.each { roleMongoId, permissions -> + def newRole = newProcessRoles[roleMongoId] + + if (!newRole && (defaultRole.stringId == roleMongoId || anonymousRole.stringId == roleMongoId)) { + log.info("Default role $roleMongoId on transition ${t.importId} detected, skipping") + oldRoles[roleMongoId] = permissions + + } else { + def oldRole = oldProcessRoles.values().find { + it.importId == newRole.importId + } + + if (!oldRole) { + log.warn("Old role does not exist for role $newRole.importId") + return + } + oldRoles[oldRole.stringId] = permissions + } + } + t.roles = oldRoles + } + + resolveDataOrder(currentNet) + + customUpdates && customUpdates.each { Closure<PetriNet> customUpdate -> + currentNet = customUpdate(currentNet, reimported) + } + + petriNetService.save(currentNet) + log.info("Migrated $currentNet.identifier") + } + + /** + * Helper method used in updateNetIgnoreRoles method, it sorts PetriNet dataSet alphabetically + * @param petriNet Instance of Petri Net + */ + void resolveDataOrder(PetriNet petriNet) { + Collator skCollator = Collator.getInstance(new Locale("sk", "SK")) + List<Field> fields = new LinkedList<>(petriNet.getDataSet().values()) + fields = fields.stream().sorted({ f1, f2 -> + int comparedTypes = f2.type.name <=> f1.type.name + if (comparedTypes != 0) return comparedTypes + return skCollator.compare((f1.name?.defaultValue ?: f1.stringId), (f2.name?.defaultValue ?: f2.stringId)) + }).collect(Collectors.toList()) + petriNet.dataSet = fields.collectEntries { [(it.getStringId()): (it)] } as LinkedHashMap<String, Field> + } + + /** + * Replaces role permissions on transition with provided map e.g. ["roleId": ["perform": true]] + * @param net Instance of Petri Net in which role on transition will be updated + * @param transitionId Transition ID of updated transition + * @param role ProcessRole that will be updated on transition + * @param permissions New role permissions on transition + */ + void updateTransitionRoles(PetriNet net, String transitionId, ProcessRole role, Map<String, Boolean> permissions) { + Transition trans = net.transitions.values().find { it.importId == transitionId } + trans.roles[role.stringId] = permissions + } + + /** + * Replaces role permissions on transition with provided map e.g. ["roleId": ["perform": true]] + * @param net Instance of Petri Net in which role on transition will be updated + * @param transitionId Transition ID of updated transition + * @param roleImportId ID of a role that will be updated on transition + * @param permissions New role permissions on transition + */ + void updateTransitionRoles(PetriNet net, String transitionId, String roleImportId, Map<String, Boolean> permissions) { + ProcessRole role = net.roles.values().find { it.importId == roleImportId } + updateTransitionRoles(net, transitionId, role, permissions) + } + + /** + * Replaces role permissions on transition with provided map e.g. ["roleId": ["perform": true]] + * @param transitionId Transition ID of updated transition + * @param roleImportId ID of a role that will be updated on transition + * @param permissions New role permissions on transition + */ + Closure<PetriNet> updateTransitionRolesClosure(String transitionId, String roleImportId, Map<String, Boolean> permissions) { + return { PetriNet petriNet, PetriNet reimported -> + updateTransitionRoles(petriNet, transitionId, roleImportId, permissions) + return petriNet + } + } + + /** + * Updates data set of existing Petri Net model with new values. + * @param identifier Identifier of Petri Net model that is being updated + * @param fileName File name of new version of Petri Net model + */ + void updateDataSet(String identifier, String fileName, Closure<PetriNet> customUpdate = null) { + PetriNet existing = petriNetService.getNewestVersionByIdentifier(identifier) + PetriNet reimported = getImporter().importPetriNet(new File("src/main/resources/petriNets/" + fileName)).get() + + reimported = replaceUserFieldRoleReferences(existing, reimported) + existing.dataSet = reimported.dataSet + + if (customUpdate) { + existing = customUpdate(existing, reimported) + } + + petriNetService.save(existing) + log.info("Migrated $identifier") + } + + /** + * Create new role in existing Petri Net model. + * @param identifier Identifier of Petri Net model in which the Process Role will be created + * @param id ID of the new Process Role + * @param title Title of the new Process Role + */ + ProcessRole createRoleInNet(String identifier, String id, String title, Map<EventType, Event> events = [:]) { + return createRoleInNet(identifier, id, new I18nString(title), events) + } + + /** + * Create new role in existing Petri Net model. + * @param identifier Identifier of Petri Net model in which the Process Role will be created + * @param id ID of the new Process Role + * @param title Title of the new Process Role + */ + ProcessRole createRoleInNet(String identifier, String id, I18nString title, Map<EventType, Event> events = [:]) { + PetriNet net = petriNetService.getNewestVersionByIdentifier(identifier) + + ProcessRole role = new ProcessRole() + role.setImportId(id) + role.setName(title) + role.setEvents(events) + + role = processRoleRepository.save(role) + net.addRole(role) + petriNetService.save(net) + + return role + } + + /** + * Updates roles of USER fields in existing Petri Net model, WARNING: new roles referenced in USER fields will be ignored! They need to be migrated manually + * @param originalNet Current Petri Net object that will be updated + * @param reimportedNet New version of Petri Net object, its values will be applied to currentNet + */ + private PetriNet replaceUserFieldRoleReferences(PetriNet originalNet, PetriNet reimportedNet) { + Map<String, ProcessRole> originalNetRoles = [:] // importId: processRole + originalNet.roles.forEach { name, role -> + originalNetRoles.put(role.importId, role) + } + + reimportedNet.dataSet.entrySet().stream().filter { + it.value.type == FieldType.USER + + }.forEach { entry -> + UserField field = (reimportedNet.dataSet[entry.key] as UserField) + field.roles = field.roles.collect { roleId -> + Optional<ProcessRole> roleOpt = Optional.ofNullable(reimportedNet.roles[roleId]) + if (roleOpt.isPresent()) { + ProcessRole oldRole = originalNetRoles[roleOpt.get().importId] + + if (!oldRole) { + log.warn("Process role in process ${originalNet.identifier} ${originalNet.stringId} with import id ${roleOpt.get().importId} not found!") + return null + + } else { + return oldRole.stringId + } + + } else { + log.warn("Role not found! ${roleId}") + return null + } + }.stream().filter { Objects.nonNull(it) }.collect() + + } + + return reimportedNet + } + + /** + * Creates new global role + * @param id ID of the new Process Role + * @param title Title of the new Process Role + */ + ProcessRole createGlobalRole(String id, String title, Map<EventType, Event> events = [:]) { + return createGlobalRole(id, new I18nString(title), events) + } + + /** + * Creates new global role + * @param id ID of the new Process Role + * @param title Title of the new Process Role + */ + ProcessRole createGlobalRole(String id, I18nString title, Map<EventType, Event> events = [:]) { + ProcessRole role = new ProcessRole() + + if (!id.startsWith("global_")) { + role.setImportId("global_" + id) + } else { + role.setImportId(id) + } + role.setName(title) + role.setEvents(events) + role.setGlobal(true) + + role = processRoleRepository.save(role) + + return role + } + + /** + * Replaces events in roles from existing with events from roles from reimported + */ + PetriNet updateRoleEvents(PetriNet existing, PetriNet reimported) { + List<ProcessRole> newRoles = reimported.roles.values() as List + List<ProcessRole> oldRoles = existing.roles.values() as List + + newRoles.each { newRole -> + ProcessRole role = oldRoles.find { it.importId == newRole.importId } + role.events = newRole.events + processRoleRepository.save(role) + } + + return existing + } + + /** + * Sets petriNet object in case instance + * @param useCase Instance of Case + * @param net Instance of Petri Net, it needs to match processIdentifier of useCase + */ + static void setPetriNet(Case useCase, PetriNet net) { + PetriNet model = net.clone() + model.initializeTokens(useCase.getActivePlaces()) + model.initializeArcs(useCase.getDataSet()) + useCase.setPetriNet(model) + } + + /** + * Provides an {@link com.netgrif.application.engine.importer.service.Importer} instance + * */ + private Importer getImporter() { + return importerProvider.get() + } } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy index d23eab7dbda..5bcc1163cd0 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy @@ -1,9 +1,16 @@ package com.netgrif.application.engine.migration.helpers +import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskMappingService +import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.TaskMigrationProperties +import com.netgrif.application.engine.petrinet.domain.PetriNet +import com.netgrif.application.engine.petrinet.domain.roles.ProcessRole import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService +import com.netgrif.application.engine.workflow.domain.Case +import com.netgrif.application.engine.workflow.domain.QTask import com.netgrif.application.engine.workflow.domain.Task +import com.netgrif.application.engine.workflow.service.interfaces.ITaskService import com.querydsl.core.types.Predicate import groovy.util.logging.Slf4j import org.springframework.data.mongodb.core.BulkOperations @@ -20,7 +27,7 @@ import org.springframework.stereotype.Component */ @Slf4j @Component -class TaskMigrationHelper extends AbstractMigrationHelper<Task>{ +class TaskMigrationHelper extends AbstractMigrationHelper<Task> { /** * The task migration properties configuration. @@ -40,6 +47,12 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task>{ */ private IPetriNetService petriNetService + private ITaskService taskService + + private IElasticTaskService elasticTaskService + + private IElasticTaskMappingService elasticTaskMappingService + /** * Constructs a new TaskMigrationHelper with the specified MongoTemplate. * @@ -47,10 +60,16 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task>{ */ TaskMigrationHelper(MongoTemplate mongoTemplate, MigrationConfigurationProperties migrationConfigurationProperties, - IPetriNetService petriNetService) { + IPetriNetService petriNetService, + ITaskService taskService, + IElasticTaskService elasticTaskService, + IElasticTaskMappingService elasticTaskMappingService) { super(Task.class, mongoTemplate) this.taskMigrationProperties = migrationConfigurationProperties.tasks this.petriNetService = petriNetService + this.taskService = taskService + this.elasticTaskService = elasticTaskService + this.elasticTaskMappingService = elasticTaskMappingService } /** @@ -65,7 +84,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task>{ int getPageSize() { return taskMigrationProperties.pageSize } - + /** * Prepares a set of bulk operations for tasks during the migration process. * @@ -139,4 +158,41 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task>{ void updateAllTasksCursor(Closure update, double pageSize = 100.0) { iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int) } + + /** + * Reloads tasks of provided case via TaskService, + * handles useCase.petriNet internally + * @param useCase Instance of Case for which tasks will be reloaded + * @param net Instance of Petri Net, it needs to match processIdentifier of useCase + */ + void reloadTasks(Case useCase, PetriNet net) { + PetriNetMigrationHelper.setPetriNet(useCase, net) + taskService.reloadTasks(useCase) + } + + /** + * Indexes provided task in elasticsearch + * @param task Instance of Task that will be indexed into elasticsearch index + */ + void elasticTaskIndex(Task task) { + try { + elasticTaskService.indexNow(elasticTaskMappingService.transform(task)) + } catch (Exception e) { + log.error("Failed to index $task.stringId", e) + } + } + + /** + * Adds role with permissions to existing tasks of net + * @param role ProcessRole that will be added to transitions + * @param net Instance of Petri Net of updated transitions + * @param transitionIds List of transition IDs the role will be added to + * @param permissions Map of permissions for the role + */ + void addRoleToExistingTasks(ProcessRole role, PetriNet net, List<String> transitionIds, Map<String, Boolean> permissions) { + updateTasks({ Task task -> + log.info("Add role '${role.getName()}' with roleId=${role.getImportId()} to transitionId=${task.getTransitionId()} in task ${task.stringId}") + task.addRole(role.getStringId(), permissions) + }, QTask.task.transitionId.in(transitionIds) & QTask.task.processId.eq(net.getStringId())) + } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java b/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java index 5734a9b3e93..2639044bce8 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java @@ -60,6 +60,7 @@ public class DataField implements Referencable, Serializable { private Long version = 0l; @Getter + @Setter private Map<String, Component> dataRefComponents; @Getter From ecefa55c97a8490b2310adceb6174b4afbb9ceec Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Wed, 27 May 2026 09:52:48 +0200 Subject: [PATCH 04/20] [NAE-2433] Implement Migration Helper Integration for Engine 6.x Refactored `CaseMigrationHelper`, `TaskMigrationHelper`, and `PetriNetMigrationHelper` to include static helper methods for flexible reuse. Added comprehensive Javadoc comments to improve maintainability and make these methods self-explanatory. Simplified `MigrationHelper` to leverage updated helpers. --- .../engine/migration/MigrationHelper.groovy | 237 +++++------------- .../helpers/CaseMigrationHelper.groovy | 103 +++++++- .../helpers/PetriNetMigrationHelper.groovy | 106 +++++++- .../helpers/TaskMigrationHelper.groovy | 53 ++++ 4 files changed, 308 insertions(+), 191 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy index f6489f795fd..6807eda069d 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy @@ -1,7 +1,5 @@ package com.netgrif.application.engine.migration -import com.netgrif.application.engine.auth.service.interfaces.IUserService -import com.netgrif.application.engine.elastic.service.interfaces.* import com.netgrif.application.engine.importer.service.Importer import com.netgrif.application.engine.migration.helpers.AbstractMigrationHelper import com.netgrif.application.engine.migration.helpers.CaseMigrationHelper @@ -9,101 +7,62 @@ import com.netgrif.application.engine.migration.helpers.PetriNetMigrationHelper import com.netgrif.application.engine.migration.helpers.TaskMigrationHelper import com.netgrif.application.engine.petrinet.domain.I18nString import com.netgrif.application.engine.petrinet.domain.PetriNet -import com.netgrif.application.engine.petrinet.domain.Transition -import com.netgrif.application.engine.petrinet.domain.VersionType -import com.netgrif.application.engine.petrinet.domain.dataset.* import com.netgrif.application.engine.petrinet.domain.events.Event import com.netgrif.application.engine.petrinet.domain.events.EventType -import com.netgrif.application.engine.petrinet.domain.repositories.PetriNetRepository import com.netgrif.application.engine.petrinet.domain.roles.ProcessRole -import com.netgrif.application.engine.petrinet.domain.roles.ProcessRoleRepository -import com.netgrif.application.engine.petrinet.service.PetriNetService -import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService -import com.netgrif.application.engine.workflow.domain.* -import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository -import com.netgrif.application.engine.workflow.domain.repositories.TaskRepository -import com.netgrif.application.engine.workflow.service.interfaces.ITaskService +import com.netgrif.application.engine.workflow.domain.Case +import com.netgrif.application.engine.workflow.domain.Task +import com.netgrif.application.engine.workflow.domain.TaskPair import com.querydsl.core.types.Predicate import groovy.util.logging.Slf4j -import org.apache.tomcat.util.http.fileupload.IOUtils -import org.bson.types.ObjectId import org.springframework.beans.factory.annotation.Autowired -import org.springframework.core.io.ClassPathResource import org.springframework.core.io.Resource -import org.springframework.data.domain.Page -import org.springframework.data.domain.PageRequest -import org.springframework.data.mongodb.core.MongoTemplate -import org.springframework.data.mongodb.core.query.Criteria -import org.springframework.data.mongodb.core.query.Query import org.springframework.stereotype.Component -import javax.inject.Provider -import java.text.Collator -import java.time.LocalDateTime -import java.util.stream.Collectors - +/** + * Helper class for migrating cases, tasks and Petri net models. + * Provides convenience methods for updating existing data and models during system migrations. + * This class delegates migration operations to specialized helper classes for cases, tasks, and Petri nets. + */ @Slf4j @Component class MigrationHelper { + /** + * Helper for case-related migration operations including updating, iterating, and indexing cases. + */ @Autowired private CaseMigrationHelper caseMigrationHelper + /** + * Helper for task-related migration operations including updating, iterating, and managing task permissions. + */ @Autowired private TaskMigrationHelper taskMigrationHelper + /** + * Helper for Petri net model migration operations including updating models, roles, and data sets. + */ @Autowired private PetriNetMigrationHelper petriNetMigrationHelper - @Autowired - private CaseRepository caseRepository - - @Autowired - private TaskRepository taskRepository - - @Autowired - private PetriNetService service - - @Autowired - private Provider<Importer> importerProvider - - @Autowired - private ProcessRoleRepository roleRepository - - @Autowired - private PetriNetRepository netRepository - - @Autowired - private ITaskService taskService - - @Autowired - private IElasticCaseService elasticCaseService - - @Autowired - private IElasticCaseMappingService caseMappingService - - @Autowired - private IElasticTaskService elasticTaskService - - @Autowired - private IElasticTaskMappingService elasticTaskMappingService - - @Autowired - private IUserService userService - - @Autowired - private IElasticIndexService elasticIndexService - - @Autowired - private MongoTemplate mongoTemplate - - @Autowired - private IPetriNetService petriNetService - + /** + * Returns the Importer service instance used for importing and processing Petri net models. + * This method delegates to the PetriNetMigrationHelper to retrieve the importer. + * @return Importer service instance + */ private Importer getImporter() { - return importerProvider.get() + return petriNetMigrationHelper.getImporter() } + /** + * Closure for updating role events between existing and reimported Petri net models. + * This closure synchronizes role-related events from the reimported model to the existing model, + * ensuring that role event configurations are properly migrated during process updates. + * @param existing The current Petri Net model that will be updated with new role events + * @param reimported The newly imported Petri Net model containing updated role event definitions + * @return Updated Petri Net model with synchronized role events + */ Closure<PetriNet> updateRoleEvents = { PetriNet existing, PetriNet reimported -> petriNetMigrationHelper.updateRoleEvents(existing, reimported) } @@ -344,7 +303,7 @@ class MigrationHelper { * @param useCase Instance of Case * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ - void setPetriNet(Case useCase, PetriNet net) { + static void setPetriNet(Case useCase, PetriNet net) { PetriNetMigrationHelper.setPetriNet(useCase, net) } @@ -353,8 +312,8 @@ class MigrationHelper { * @param useCase Instance of Case * @param toDelete List of field IDs that will be deleted from useCase */ - void deleteDataFields(Case useCase, List<String> toDelete) { - caseMigrationHelper.deleteDataFields(useCase, toDelete) + static void deleteDataFields(Case useCase, List<String> toDelete) { + CaseMigrationHelper.deleteDataFields(useCase, toDelete) } /** @@ -362,8 +321,8 @@ class MigrationHelper { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - void changeDataFieldsValueFromNumberToText(Case useCase, List<String> toChange) { - caseMigrationHelper.changeDataFieldsValueFromNumberToText(useCase, toChange) + static void changeDataFieldsValueFromNumberToText(Case useCase, List<String> toChange) { + CaseMigrationHelper.changeDataFieldsValueFromNumberToText(useCase, toChange) } /** @@ -371,8 +330,8 @@ class MigrationHelper { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - void changeDataFieldsValueFromTextToNumber(Case useCase, List<String> toChange) { - caseMigrationHelper.changeDataFieldsValueFromTextToNumber(useCase, toChange) + static void changeDataFieldsValueFromTextToNumber(Case useCase, List<String> toChange) { + CaseMigrationHelper.changeDataFieldsValueFromTextToNumber(useCase, toChange) } /** @@ -380,8 +339,8 @@ class MigrationHelper { * @param useCase Instance of Case * @param toAdd Map<field id, init value of field> */ - void addTextDataFields(Case useCase, Map<String, String> toAdd) { - caseMigrationHelper.addTextDataFields(useCase, toAdd) + static void addTextDataFields(Case useCase, Map<String, String> toAdd) { + CaseMigrationHelper.addTextDataFields(useCase, toAdd) } /** @@ -389,8 +348,8 @@ class MigrationHelper { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - void changeDataFieldsValueFromEnumerationToMultichoice(Case useCase, List<String> toChange) { - caseMigrationHelper.changeDataFieldsValueFromEnumerationToMultichoice(useCase, toChange) + static void changeDataFieldsValueFromEnumerationToMultichoice(Case useCase, List<String> toChange) { + CaseMigrationHelper.changeDataFieldsValueFromEnumerationToMultichoice(useCase, toChange) } /** @@ -398,8 +357,8 @@ class MigrationHelper { * @param useCase Instance of Case * @param toAdd Map<field id, list of choices to add into data data field> */ - void addChoices(Case useCase, Map<String, List<String>> toAdd) { - caseMigrationHelper.addChoices(useCase, toAdd) + static void addChoices(Case useCase, Map<String, List<String>> toAdd) { + CaseMigrationHelper.addChoices(useCase, toAdd) } /** @@ -407,8 +366,8 @@ class MigrationHelper { * @param useCase Instance of Case * @param toAdd Map<field id, list of choices to add into data field> */ - void removeChoices(Case useCase, Map<String, List<String>> toRemove) { - caseMigrationHelper.removeChoices(useCase, toRemove) + static void removeChoices(Case useCase, Map<String, List<String>> toRemove) { + CaseMigrationHelper.removeChoices(useCase, toRemove) } /** @@ -416,25 +375,16 @@ class MigrationHelper { * @param useCase Instance of Case * @param fieldId Field ID for value change */ - private void changeFileFieldToFileList(Case useCase, String fieldId) { - FileListFieldValue fileListFieldValue = new FileListFieldValue() - fileListFieldValue.namesPaths.add(useCase.dataSet[fieldId].value as FileFieldValue) - useCase.dataSet[fieldId].value = fileListFieldValue + static void changeFileFieldToFileList(Case useCase, String fieldId) { + CaseMigrationHelper.changeFileFieldToFileList(useCase, fieldId) } /** * Helper method used in updateNetIgnoreRoles method, it sorts PetriNet dataSet alphabetically * @param petriNet Instance of Petri Net */ - void resolveDataOrder(PetriNet petriNet) { - Collator skCollator = Collator.getInstance(new Locale("sk", "SK")) - List<Field> fields = new LinkedList<>(petriNet.getDataSet().values()) - fields = fields.stream().sorted({ f1, f2 -> - int comparedTypes = f2.type.name <=> f1.type.name - if (comparedTypes != 0) return comparedTypes - return skCollator.compare((f1.name?.defaultValue ?: f1.stringId), (f2.name?.defaultValue ?: f2.stringId)) - }).collect(Collectors.toList()) - petriNet.dataSet = fields.collectEntries { [(it.getStringId()): (it)] } as Map<String, Field> + static void resolveDataOrder(PetriNet petriNet) { + PetriNetMigrationHelper.resolveDataOrder(petriNet) } /** @@ -442,56 +392,24 @@ class MigrationHelper { * @param useCase Instance of Case * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ - void updateCaseComponents(Case useCase, PetriNet net) { - Map<String, com.netgrif.application.engine.petrinet.domain.Component> components = createComponentsMap(net) - Map<String, Map<String, com.netgrif.application.engine.petrinet.domain.Component>> dataRefComponents = createDataRefComponentsMap(net) - - useCase.dataSet.each {dataField -> - if (components[dataField.key]) { - useCase.dataSet[dataField.key].component = components[dataField.key] - } - if (dataRefComponents[dataField.key]) { - useCase.dataSet[dataField.key].dataRefComponents = dataRefComponents[dataField.key] - } - } + static void updateCaseComponents(Case useCase, PetriNet net) { + CaseMigrationHelper.updateCaseComponents(useCase, net) } /** * Method that collects all dataRef components of given PetriNet. Should be used in updateCases method, when a new dataRef component is added into PetriNet. * @param net Instance of PetriNet */ - Map<String, Map<String, com.netgrif.application.engine.petrinet.domain.Component>> createDataRefComponentsMap(PetriNet net) { - Map<String, Map<String, com.netgrif.application.engine.petrinet.domain.Component>> componentsMap = [:] - net.transitions.each {transition -> - String transId = transition.key - transition.value.dataSet.each {dataField -> - String fieldId = dataField.key - if (dataField.value.component) { - if (!componentsMap[fieldId]) { - componentsMap.put(fieldId, [(transId) : dataField.value.component]) - } else { - Map<String, com.netgrif.application.engine.petrinet.domain.Component> existingMap = componentsMap[fieldId] - existingMap.put(transId, dataField.value.component) - componentsMap.put(fieldId, existingMap) - } - } - } - } - return componentsMap + static Map<String, Map<String, com.netgrif.application.engine.petrinet.domain.Component>> createDataRefComponentsMap(PetriNet net) { + PetriNetMigrationHelper.createDataRefComponentsMap(net) } /** * Method that collects all dataField components of given PetriNet. Should be used in updateCases method, when a new dataField component is added into PetriNet. * @param net Instance of PetriNet */ - Map<String, com.netgrif.application.engine.petrinet.domain.Component> createComponentsMap(PetriNet net) { - Map<String, com.netgrif.application.engine.petrinet.domain.Component> componentsMap = [:] - net.dataSet.each {dataField -> - if (dataField.value.component) { - componentsMap.put(dataField.key, dataField.value.component) - } - } - return componentsMap + static Map<String, com.netgrif.application.engine.petrinet.domain.Component> createComponentsMap(PetriNet net) { + PetriNetMigrationHelper.createComponentsMap(net) } /** @@ -499,26 +417,8 @@ class MigrationHelper { * @param useCase Instance of Case * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ - void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false) { - useCase.permissions = net.getPermissions().entrySet().stream() - .filter(role -> role.getValue().containsKey("delete") || role.getValue().containsKey("view")) - .map(role -> { - Map<String, Boolean> permissionMap = new HashMap<>() - if (role.getValue().containsKey("delete")) - permissionMap.put("delete", role.getValue().get("delete")) - if (role.getValue().containsKey("view")) { - permissionMap.put("view", role.getValue().get("view")) - } - return new AbstractMap.SimpleEntry<>(role.getKey(), permissionMap) - }) - .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)) - useCase.resolveViewRoles() - useCase.setEnabledRoles(net.getRoles().keySet()) - if (updateTasks) { - useCase.tasks.each { taskPair -> - updateTaskPermissions(useCase, taskPair, net) - } - } + static void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false) { + CaseMigrationHelper.updateCasePermissionsFromNet(useCase, net, updateTasks) } /** @@ -528,9 +428,7 @@ class MigrationHelper { * @param relevantTransitionIds List of transition IDs for permissions update */ void updateTasksPermissions(Case useCase, PetriNet net, List<String> relevantTransitionIds) { - useCase.tasks.findAll { it.transition in relevantTransitionIds }.each { taskPair -> - updateTaskPermissions(useCase, taskPair, net) - } + taskMigrationHelper.updateTasksPermissions(useCase, net, relevantTransitionIds) } /** @@ -540,18 +438,7 @@ class MigrationHelper { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ void updateTaskPermissions(Case useCase, TaskPair taskPair, PetriNet net) { - try { - Transition newTransition = net.getTransition(taskPair.transition) - Task oldTask = taskService.findOne(taskPair.task) - oldTask.setProcessId(net.stringId) - oldTask.getRoles().clear() - oldTask.setRoles(newTransition.roles) - oldTask.setNegativeViewRoles(newTransition.negativeViewRoles) - oldTask.resolveViewRoles() - taskService.save(oldTask) - } catch (Exception e) { - log.error("Failed to update task permissions $useCase.stringId $taskPair.transition", e) - } + taskMigrationHelper.updateTaskPermissions(useCase, taskPair, net) } /** @@ -559,7 +446,7 @@ class MigrationHelper { * @param useCase Instance of Case * @param newNet Instance of Petri Net, it needs to match processIdentifier of useCase */ - void migratePetriNet(Case useCase, PetriNet newNet) { - useCase.setPetriNetObjectId(newNet.objectId) + static void migratePetriNet(Case useCase, PetriNet newNet) { + CaseMigrationHelper.migratePetriNet(useCase, newNet) } } \ No newline at end of file diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index 252d137cbda..d2b943af8f2 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -1,11 +1,13 @@ package com.netgrif.application.engine.migration.helpers -import com.netgrif.application.engine.elastic.service.ElasticCaseMappingService import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseMappingService import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.CaseMigrationProperties import com.netgrif.application.engine.petrinet.domain.I18nString +import com.netgrif.application.engine.petrinet.domain.PetriNet +import com.netgrif.application.engine.petrinet.domain.dataset.FileFieldValue +import com.netgrif.application.engine.petrinet.domain.dataset.FileListFieldValue import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService import com.netgrif.application.engine.workflow.domain.Case import com.netgrif.application.engine.workflow.domain.DataField @@ -18,6 +20,7 @@ import org.springframework.data.mongodb.core.query.Query import org.springframework.stereotype.Component import java.time.LocalDateTime +import java.util.stream.Collectors /** * Helper class for managing migrations of Case objects in the application. @@ -36,12 +39,26 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { */ private CaseMigrationProperties caseMigrationProperties + /** + * Service for managing PetriNet operations. + */ private IPetriNetService petriNetService + /** + * Service for indexing and managing cases in Elasticsearch. + */ private IElasticCaseService elasticCaseService + /** + * Service for mapping Case objects to Elasticsearch documents. + */ private IElasticCaseMappingService elasticCaseMappingService + /** + * Helper for managing task migrations associated with cases. + */ + private TaskMigrationHelper taskMigrationHelper + /** * Constructs a CaseMigrationHelper instance with * the provided MongoTemplate and migration configuration properties. @@ -53,12 +70,14 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { MigrationConfigurationProperties migrationConfigurationProperties, IPetriNetService petriNetService, IElasticCaseService elasticCaseService, - IElasticCaseMappingService elasticCaseMappingService) { + IElasticCaseMappingService elasticCaseMappingService, + TaskMigrationHelper taskMigrationHelper) { super(Case.class, mongoTemplate) this.caseMigrationProperties = migrationConfigurationProperties.cases this.petriNetService = petriNetService this.elasticCaseService = elasticCaseService this.elasticCaseMappingService = elasticCaseMappingService + this.taskMigrationHelper = taskMigrationHelper } /** @@ -160,7 +179,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toDelete List of field IDs that will be deleted from useCase */ - void deleteDataFields(Case useCase, List<String> toDelete) { + static void deleteDataFields(Case useCase, List<String> toDelete) { toDelete.each { dataFieldID -> useCase.dataSet.remove(dataFieldID) } @@ -171,7 +190,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - void changeDataFieldsValueFromNumberToText(Case useCase, List<String> toChange) { + static void changeDataFieldsValueFromNumberToText(Case useCase, List<String> toChange) { toChange.each { dataFieldID -> if (useCase.dataSet[dataFieldID].value && (useCase.dataSet[dataFieldID].value != null || useCase.dataSet[dataFieldID].value != "")) { double value = useCase.dataSet[dataFieldID].value as double @@ -185,7 +204,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - void changeDataFieldsValueFromTextToNumber(Case useCase, List<String> toChange) { + static void changeDataFieldsValueFromTextToNumber(Case useCase, List<String> toChange) { toChange.each { dataFieldID -> if (useCase.dataSet[dataFieldID].value && useCase.dataSet[dataFieldID].value != "") { try { @@ -203,7 +222,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toAdd Map<field id, init value of field> */ - void addTextDataFields(Case useCase, Map<String, String> toAdd) { + static void addTextDataFields(Case useCase, Map<String, String> toAdd) { toAdd.each { dataFieldID, value -> useCase.dataSet[dataFieldID] = new DataField(value) } @@ -214,7 +233,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - void changeDataFieldsValueFromEnumerationToMultichoice(Case useCase, List<String> toChange) { + static void changeDataFieldsValueFromEnumerationToMultichoice(Case useCase, List<String> toChange) { toChange.each { dataFieldID -> if (useCase.dataSet[dataFieldID].value && useCase.dataSet[dataFieldID].value != null) { def value @@ -236,7 +255,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toAdd Map<field id, list of choices to add into data data field> */ - void addChoices(Case useCase, Map<String, List<String>> toAdd) { + static void addChoices(Case useCase, Map<String, List<String>> toAdd) { toAdd.each { dataFieldID, newChoices -> if (useCase.dataSet[dataFieldID].choices == null) { useCase.dataSet[dataFieldID].setChoices(new HashSet<I18nString>()) @@ -253,7 +272,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toAdd Map<field id, list of choices to add into data field> */ - void removeChoices(Case useCase, Map<String, List<String>> toRemove) { + static void removeChoices(Case useCase, Map<String, List<String>> toRemove) { toRemove.each { dataFieldID, choicesToRemove -> if (useCase.dataSet[dataFieldID].value != null) { (useCase.dataSet[dataFieldID].value as Set).removeAll(choicesToRemove) @@ -264,5 +283,71 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { } } } + + /** + * Changes value from FileFieldValue to FileListFieldValue + * @param useCase Instance of Case + * @param fieldId Field ID for value change + */ + static void changeFileFieldToFileList(Case useCase, String fieldId) { + FileListFieldValue fileListFieldValue = new FileListFieldValue() + fileListFieldValue.namesPaths.add(useCase.dataSet[fieldId].value as FileFieldValue) + useCase.dataSet[fieldId].value = fileListFieldValue + } + + /** + * Update dataField and dataRef components of given case + * @param useCase Instance of Case + * @param net Instance of Petri Net, it needs to match processIdentifier of useCase + */ + static void updateCaseComponents(Case useCase, PetriNet net) { + Map<String, com.netgrif.application.engine.petrinet.domain.Component> components = PetriNetMigrationHelper.createComponentsMap(net) + Map<String, Map<String, com.netgrif.application.engine.petrinet.domain.Component>> dataRefComponents = PetriNetMigrationHelper.createDataRefComponentsMap(net) + + useCase.dataSet.each {dataField -> + if (components[dataField.key]) { + useCase.dataSet[dataField.key].component = components[dataField.key] + } + if (dataRefComponents[dataField.key]) { + useCase.dataSet[dataField.key].dataRefComponents = dataRefComponents[dataField.key] + } + } + } + + /** + * Updates case permissions from PetriNet + * @param useCase Instance of Case + * @param net Instance of Petri Net, it needs to match processIdentifier of useCase + */ + static void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false) { + useCase.permissions = net.getPermissions().entrySet().stream() + .filter(role -> role.getValue().containsKey("delete") || role.getValue().containsKey("view")) + .map(role -> { + Map<String, Boolean> permissionMap = new HashMap<>() + if (role.getValue().containsKey("delete")) + permissionMap.put("delete", role.getValue().get("delete")) + if (role.getValue().containsKey("view")) { + permissionMap.put("view", role.getValue().get("view")) + } + return new AbstractMap.SimpleEntry<>(role.getKey(), permissionMap) + }) + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)) + useCase.resolveViewRoles() + useCase.setEnabledRoles(net.getRoles().keySet()) + if (updateTasks) { + useCase.tasks.each { taskPair -> + taskMigrationHelper.updateTaskPermissions(useCase, taskPair, net) + } + } + } + + /** + * Changes PetriNet reference in useCase + * @param useCase Instance of Case + * @param newNet Instance of Petri Net, it needs to match processIdentifier of useCase + */ + static void migratePetriNet(Case useCase, PetriNet newNet) { + useCase.setPetriNetObjectId(newNet.objectId) + } } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy index 3706db19465..4f51f9295b6 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy @@ -31,22 +31,58 @@ import javax.inject.Provider import java.text.Collator import java.util.stream.Collectors +/** + * Helper class for managing Petri Net migration operations in MongoDB. + * <p> + * This component provides utilities for migrating and updating Petri Net models, including: + * <ul> + * <li>Updating existing Petri Nets while preserving role references</li> + * <li>Managing process roles and permissions</li> + * <li>Handling data set migrations</li> + * <li>Creating and updating roles in Petri Net models</li> + * <li>Bulk operations for efficient database updates</li> + * </ul> + * <p> + * The helper extends {@link AbstractMigrationHelper} to leverage common migration patterns + * and uses Spring Data MongoDB for database operations. + * + * @see AbstractMigrationHelper* @see PetriNet* @see IPetriNetService* @see ProcessRoleRepository + */ @Slf4j @Component class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { + + /** + * Configuration properties specific to Petri Net migration operations. + * Contains settings such as page size and other Petri Net-related migration configurations. + */ private PetriNetMigrationProperties petriNetMigrationProperties + /** + * Service interface for managing Petri Net operations including importing, saving, and retrieving Petri Net models. + */ private IPetriNetService petriNetService + /** + * Repository for persisting and retrieving process roles from the database. + */ private ProcessRoleRepository processRoleRepository + /** + * Provider that supplies {@link Importer} instances for importing Petri Net models from various sources. + * Uses lazy initialization to create Importer instances on demand. + */ private Provider<Importer> importerProvider /** - * Constructs a new PetriNetMigrationHelper with the specified MongoTemplate. + * Constructs a new PetriNetMigrationHelper with the specified dependencies. * * @param mongoTemplate the {@link MongoTemplate} to use for interacting with MongoDB + * @param migrationConfigurationProperties the {@link MigrationConfigurationProperties} containing migration settings including page size and other configuration + * @param petriNetService the {@link IPetriNetService} for managing Petri Net operations such as importing, saving, and retrieving Petri Nets + * @param processRoleRepository the {@link ProcessRoleRepository} for persisting and retrieving process roles from the database + * @param importerProvider the {@link Provider} that supplies {@link Importer} instances for importing Petri Net models from various sources */ PetriNetMigrationHelper(MongoTemplate mongoTemplate, MigrationConfigurationProperties migrationConfigurationProperties, @@ -60,11 +96,23 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { this.importerProvider = importerProvider } + /** + * Returns the page size for pagination during migration operations. + * + * @return the page size configured in {@link PetriNetMigrationProperties} + */ @Override int getPageSize() { return petriNetMigrationProperties.pageSize } + /** + * Prepares bulk operations for updating a Petri Net document in MongoDB. + * + * @param document the {@link PetriNet} document to be updated + * @param update the closure that performs the update operation on the document + * @param bulkOperations the {@link BulkOperations} to add the replace operation to + */ @Override void prepareOperations(PetriNet document, Closure update, BulkOperations bulkOperations) { log.debug("Updating case with ID ${document.stringId}") @@ -101,6 +149,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * Updates existing Petri Net model with new values. New process roles are ignored! New roles in existing user type fields will be ignored! * @param currentNet Current Petri Net object that will be updated * @param reimported New version of Petri Net object, its values will be applied to currentNet + * @param customUpdates Optional list of custom update closures to be applied after the standard update */ void updateNetIgnoreRoles(PetriNet currentNet, PetriNet reimported, List<Closure<PetriNet>> customUpdates) { if (!currentNet) { @@ -187,7 +236,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * Helper method used in updateNetIgnoreRoles method, it sorts PetriNet dataSet alphabetically * @param petriNet Instance of Petri Net */ - void resolveDataOrder(PetriNet petriNet) { + static void resolveDataOrder(PetriNet petriNet) { Collator skCollator = Collator.getInstance(new Locale("sk", "SK")) List<Field> fields = new LinkedList<>(petriNet.getDataSet().values()) fields = fields.stream().sorted({ f1, f2 -> @@ -205,7 +254,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param role ProcessRole that will be updated on transition * @param permissions New role permissions on transition */ - void updateTransitionRoles(PetriNet net, String transitionId, ProcessRole role, Map<String, Boolean> permissions) { + static void updateTransitionRoles(PetriNet net, String transitionId, ProcessRole role, Map<String, Boolean> permissions) { Transition trans = net.transitions.values().find { it.importId == transitionId } trans.roles[role.stringId] = permissions } @@ -217,7 +266,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param roleImportId ID of a role that will be updated on transition * @param permissions New role permissions on transition */ - void updateTransitionRoles(PetriNet net, String transitionId, String roleImportId, Map<String, Boolean> permissions) { + static void updateTransitionRoles(PetriNet net, String transitionId, String roleImportId, Map<String, Boolean> permissions) { ProcessRole role = net.roles.values().find { it.importId == roleImportId } updateTransitionRoles(net, transitionId, role, permissions) } @@ -228,7 +277,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param roleImportId ID of a role that will be updated on transition * @param permissions New role permissions on transition */ - Closure<PetriNet> updateTransitionRolesClosure(String transitionId, String roleImportId, Map<String, Boolean> permissions) { + static Closure<PetriNet> updateTransitionRolesClosure(String transitionId, String roleImportId, Map<String, Boolean> permissions) { return { PetriNet petriNet, PetriNet reimported -> updateTransitionRoles(petriNet, transitionId, roleImportId, permissions) return petriNet @@ -291,8 +340,9 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * Updates roles of USER fields in existing Petri Net model, WARNING: new roles referenced in USER fields will be ignored! They need to be migrated manually * @param originalNet Current Petri Net object that will be updated * @param reimportedNet New version of Petri Net object, its values will be applied to currentNet + * @return the updated reimported Petri Net with replaced role references */ - private PetriNet replaceUserFieldRoleReferences(PetriNet originalNet, PetriNet reimportedNet) { + private static PetriNet replaceUserFieldRoleReferences(PetriNet originalNet, PetriNet reimportedNet) { Map<String, ProcessRole> originalNetRoles = [:] // importId: processRole originalNet.roles.forEach { name, role -> originalNetRoles.put(role.importId, role) @@ -360,6 +410,9 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { /** * Replaces events in roles from existing with events from roles from reimported + * @param existing the existing {@link PetriNet} whose role events will be updated + * @param reimported the reimported {@link PetriNet} containing new role events + * @return the updated existing Petri Net */ PetriNet updateRoleEvents(PetriNet existing, PetriNet reimported) { List<ProcessRole> newRoles = reimported.roles.values() as List @@ -388,8 +441,47 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { /** * Provides an {@link com.netgrif.application.engine.importer.service.Importer} instance + * @return a new {@link Importer} instance from the provider * */ - private Importer getImporter() { + Importer getImporter() { return importerProvider.get() } + + /** + * Method that collects all dataRef components of given PetriNet. Should be used in updateCases method, when a new dataRef component is added into PetriNet. + * @param net Instance of PetriNet + */ + static Map<String, Map<String, com.netgrif.application.engine.petrinet.domain.Component>> createDataRefComponentsMap(PetriNet net) { + Map<String, Map<String, com.netgrif.application.engine.petrinet.domain.Component>> componentsMap = [:] + net.transitions.each {transition -> + String transId = transition.key + transition.value.dataSet.each {dataField -> + String fieldId = dataField.key + if (dataField.value.component) { + if (!componentsMap[fieldId]) { + componentsMap.put(fieldId, [(transId) : dataField.value.component]) + } else { + Map<String, com.netgrif.application.engine.petrinet.domain.Component> existingMap = componentsMap[fieldId] + existingMap.put(transId, dataField.value.component) + componentsMap.put(fieldId, existingMap) + } + } + } + } + return componentsMap + } + + /** + * Method that collects all dataField components of given PetriNet. Should be used in updateCases method, when a new dataField component is added into PetriNet. + * @param net Instance of PetriNet + */ + static Map<String, com.netgrif.application.engine.petrinet.domain.Component> createComponentsMap(PetriNet net) { + Map<String, com.netgrif.application.engine.petrinet.domain.Component> componentsMap = [:] + net.dataSet.each {dataField -> + if (dataField.value.component) { + componentsMap.put(dataField.key, dataField.value.component) + } + } + return componentsMap + } } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy index 5bcc1163cd0..915e19ea8bb 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy @@ -5,11 +5,13 @@ import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskSer import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.TaskMigrationProperties import com.netgrif.application.engine.petrinet.domain.PetriNet +import com.netgrif.application.engine.petrinet.domain.Transition import com.netgrif.application.engine.petrinet.domain.roles.ProcessRole import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService import com.netgrif.application.engine.workflow.domain.Case import com.netgrif.application.engine.workflow.domain.QTask import com.netgrif.application.engine.workflow.domain.Task +import com.netgrif.application.engine.workflow.domain.TaskPair import com.netgrif.application.engine.workflow.service.interfaces.ITaskService import com.querydsl.core.types.Predicate import groovy.util.logging.Slf4j @@ -47,10 +49,28 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { */ private IPetriNetService petriNetService + /** + * Service for handling task operations. + * + * This service provides methods for managing task entities, + * including finding, saving, and reloading tasks during migration processes. + */ private ITaskService taskService + /** + * Service for handling Elasticsearch task indexing operations. + * + * This service is used to index task documents into Elasticsearch, + * enabling full-text search and analytics capabilities for tasks. + */ private IElasticTaskService elasticTaskService + /** + * Service for mapping task entities to Elasticsearch documents. + * + * This service transforms task domain objects into their Elasticsearch + * representation before indexing, ensuring proper field mapping and data structure. + */ private IElasticTaskMappingService elasticTaskMappingService /** @@ -195,4 +215,37 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { task.addRole(role.getStringId(), permissions) }, QTask.task.transitionId.in(transitionIds) & QTask.task.processId.eq(net.getStringId())) } + + /** + * Updates permissions on existing tasks filtered by relevantTransitionIds + * @param useCase Instance of Case + * @param net Instance of Petri Net, it needs to match processIdentifier of useCase + * @param relevantTransitionIds List of transition IDs for permissions update + */ + void updateTasksPermissions(Case useCase, PetriNet net, List<String> relevantTransitionIds) { + useCase.tasks.findAll { it.transition in relevantTransitionIds }.each { taskPair -> + updateTaskPermissions(useCase, taskPair, net) + } + } + + /** + * Updates permissions on existing task + * @param useCase Instance of Case + * @param taskPair TaskPair object of updated Task + * @param net Instance of Petri Net, it needs to match processIdentifier of useCase + */ + void updateTaskPermissions(Case useCase, TaskPair taskPair, PetriNet net) { + try { + Transition newTransition = net.getTransition(taskPair.transition) + Task oldTask = taskService.findOne(taskPair.task) + oldTask.setProcessId(net.stringId) + oldTask.getRoles().clear() + oldTask.setRoles(newTransition.roles) + oldTask.setNegativeViewRoles(newTransition.negativeViewRoles) + oldTask.resolveViewRoles() + taskService.save(oldTask) + } catch (Exception e) { + log.error("Failed to update task permissions $useCase.stringId $taskPair.transition", e) + } + } } From ba219b18a94ee3de0cdc41a14fb526d17d4875ce Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Wed, 27 May 2026 12:19:45 +0200 Subject: [PATCH 05/20] Remove unused `MongodbSerializer` class --- .../engine/mongodb/MongodbSerializer.java | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/main/java/com/netgrif/application/engine/mongodb/MongodbSerializer.java diff --git a/src/main/java/com/netgrif/application/engine/mongodb/MongodbSerializer.java b/src/main/java/com/netgrif/application/engine/mongodb/MongodbSerializer.java deleted file mode 100644 index a1b38e6736d..00000000000 --- a/src/main/java/com/netgrif/application/engine/mongodb/MongodbSerializer.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.netgrif.application.engine.mongodb; - -import com.mongodb.DBRef; -import com.querydsl.core.types.Path; -import com.querydsl.mongodb.document.MongodbDocumentSerializer; - -public class MongodbSerializer extends MongodbDocumentSerializer { - - @Override - protected DBRef asReference(Object o) { - return null; - } - - @Override - protected boolean isReference(Path<?> path) { - return false; - } -} From f1f2cc0ebc06c4cbbc6dda5a3af889bd6b1020ea Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Thu, 28 May 2026 09:13:21 +0200 Subject: [PATCH 06/20] [NAE-2433] Implement Migration Helper Integration for Engine 6.x Optimized test setup to reduce case count from 10,000 to 100 for efficiency. Removed deprecated legacy migration code and file-writing logic. Enhanced assertions to validate updated data fields and task permissions post-migration. --- .../migration/MigrationBenchmarkTest.groovy | 70 +++++++------------ 1 file changed, 26 insertions(+), 44 deletions(-) diff --git a/src/test/groovy/com/netgrif/application/engine/migration/MigrationBenchmarkTest.groovy b/src/test/groovy/com/netgrif/application/engine/migration/MigrationBenchmarkTest.groovy index 795b0a64eaa..75194cb8c49 100644 --- a/src/test/groovy/com/netgrif/application/engine/migration/MigrationBenchmarkTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/migration/MigrationBenchmarkTest.groovy @@ -7,6 +7,8 @@ import com.netgrif.application.engine.petrinet.domain.VersionType import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService import com.netgrif.application.engine.startup.SuperCreator import com.netgrif.application.engine.workflow.domain.Case +import com.netgrif.application.engine.workflow.domain.DataField +import com.netgrif.application.engine.workflow.domain.QCase import com.netgrif.application.engine.workflow.domain.eventoutcomes.petrinetoutcomes.ImportPetriNetEventOutcome import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService import groovy.util.logging.Slf4j @@ -16,7 +18,8 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.domain.Pageable; import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.junit.jupiter.SpringExtension @@ -51,15 +54,6 @@ class MigrationBenchmarkTest { private static FileWriter writer - @BeforeAll - static void beforeAll() { - File report = new File("src/main/resources/migration_report.txt") - if (report.createNewFile()) { - log.info("New migration report file created") - } - writer = new FileWriter(report) - } - @BeforeEach void beforeEach() { testHelper.truncateDbs() @@ -72,48 +66,36 @@ class MigrationBenchmarkTest { assert netV2Outcome.getNet() != null netV2 = netV2Outcome.getNet() - (1..10000).stream().parallel().forEach { + (1..100).stream().parallel().forEach { workflowService.createCase(netV1.stringId, "Net V1 " + it, null, superCreator.loggedSuper, Locale.default) } } - @AfterAll - static void afterAll() { - writer.close() - } - - //TODO: to be deleted - @Test - void migrateCasesWitLegacyCursor() { - LocalDateTime startOfLegacyMigration = LocalDateTime.now() - migrationHelper.updateCasesCursor({ Case useCase -> - migrationHelper.updateCasePermissionsFromNet(useCase, netV2) - }, "nae_2432") - LocalDateTime endOfLegacyMigration = LocalDateTime.now() - Duration diff = Duration.between(startOfLegacyMigration, endOfLegacyMigration) - writer.write("==============================\n") - writer.write("LEGACY MIGRATION HELPER\n") - writer.write("Migrated 10000 cases\n") - writer.write("Started at " + startOfLegacyMigration.toString() + "\n") - writer.write("Ended at " + endOfLegacyMigration.toString() + "\n") - writer.write("Duration: " + diff.toString() + "\n") - writer.write("==============================\n") - } - @Test void migrateCasesWithCursor() { - LocalDateTime startOfLegacyMigration = LocalDateTime.now() + List<Case> caseList = workflowService.search(QCase.case$.processIdentifier.eq("nae_2432"), Pageable.ofSize(100)).getContent() + caseList.forEach { + assert !it.dataSet.containsKey("income") + assert !it.dataSet.containsKey("recreate_info_text") + assert it.enabledRoles.size() == 0 + assert it.tasks.size() == 1 && it.tasks[0].transition == "person_info" + } + caseMigrationHelper.updateCasesCursor({ Case useCase -> migrationHelper.updateCasePermissionsFromNet(useCase, netV2) + migrationHelper.updateTasksPermissions(useCase, netV2, ["t1", "t2"]) + migrationHelper.reloadTasks(useCase, netV2) + + useCase.dataSet["income"] = new DataField(0) + useCase.dataSet["recreate_info_text"] = new DataField("") + }, "nae_2432") - LocalDateTime endOfLegacyMigration = LocalDateTime.now() - Duration diff = Duration.between(startOfLegacyMigration, endOfLegacyMigration) - writer.write("==============================\n") - writer.write("NEW MIGRATION HELPER\n") - writer.write("Migrated 10000 cases\n") - writer.write("Started at " + startOfLegacyMigration.toString() + "\n") - writer.write("Ended at " + endOfLegacyMigration.toString() + "\n") - writer.write("Duration: " + diff.toString() + "\n") - writer.write("==============================\n") + caseList = workflowService.search(QCase.case$.processIdentifier.eq("nae_2432"), Pageable.ofSize(100)).getContent() + caseList.forEach { + assert it.dataSet.containsKey("income") + assert it.dataSet.containsKey("recreate_info_text") + assert it.enabledRoles.size() == 5 + assert it.tasks.size() == 2 && it.tasks[0].transition == "person_info" && it.tasks[1].transition == "recreate_person" + } } } From a40bf41c0d38d91324e52ebe318744152f29fd82 Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Thu, 28 May 2026 10:52:27 +0200 Subject: [PATCH 07/20] [NAE-2433] Implement Migration Helper Integration for Engine 6.x - removed unused data annotation from MigrationConfigurationProperties - Add Elasticsearch indexing helpers for cases and tasks with safer error handling - Use configured PetriNet references when indexing cases - Support task permission updates when migrating case permissions - Harden case data field migration utilities against null values and conversion failures - Add helper methods for task reloads, role assignment, and permission synchronization - Extend PetriNet migration utilities with custom update hooks and role event updates - Make migration helper dependencies immutable - Replace Lombok-based migration configuration properties with explicit accessors - Update migration tests for the enhanced helper behavior --- .../engine/migration/MigrationHelper.groovy | 12 +-- .../MigrationConfigurationProperties.groovy | 5 +- .../helpers/AbstractMigrationHelper.groovy | 14 ++- .../helpers/CaseMigrationHelper.groovy | 96 +++++++++++-------- .../helpers/PetriNetMigrationHelper.groovy | 50 +++++++--- .../helpers/TaskMigrationHelper.groovy | 14 ++- ...chmarkTest.groovy => MigrationTest.groovy} | 31 +++--- 7 files changed, 129 insertions(+), 93 deletions(-) rename src/test/groovy/com/netgrif/application/engine/migration/{MigrationBenchmarkTest.groovy => MigrationTest.groovy} (77%) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy index 6807eda069d..47a114d18ea 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy @@ -312,7 +312,7 @@ class MigrationHelper { * @param useCase Instance of Case * @param toDelete List of field IDs that will be deleted from useCase */ - static void deleteDataFields(Case useCase, List<String> toDelete) { + static void deleteDataFields(Case useCase, Set<String> toDelete) { CaseMigrationHelper.deleteDataFields(useCase, toDelete) } @@ -321,7 +321,7 @@ class MigrationHelper { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - static void changeDataFieldsValueFromNumberToText(Case useCase, List<String> toChange) { + static void changeDataFieldsValueFromNumberToText(Case useCase, Set<String> toChange) { CaseMigrationHelper.changeDataFieldsValueFromNumberToText(useCase, toChange) } @@ -330,7 +330,7 @@ class MigrationHelper { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - static void changeDataFieldsValueFromTextToNumber(Case useCase, List<String> toChange) { + static void changeDataFieldsValueFromTextToNumber(Case useCase, Set<String> toChange) { CaseMigrationHelper.changeDataFieldsValueFromTextToNumber(useCase, toChange) } @@ -348,7 +348,7 @@ class MigrationHelper { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - static void changeDataFieldsValueFromEnumerationToMultichoice(Case useCase, List<String> toChange) { + static void changeDataFieldsValueFromEnumerationToMultichoice(Case useCase, Set<String> toChange) { CaseMigrationHelper.changeDataFieldsValueFromEnumerationToMultichoice(useCase, toChange) } @@ -417,8 +417,8 @@ class MigrationHelper { * @param useCase Instance of Case * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ - static void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false) { - CaseMigrationHelper.updateCasePermissionsFromNet(useCase, net, updateTasks) + void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false) { + caseMigrationHelper.updateCasePermissionsFromNet(useCase, net, updateTasks) } /** diff --git a/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy b/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy index 1813b2eec30..302300fa357 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy @@ -1,6 +1,6 @@ package com.netgrif.application.engine.migration.config.properties -import lombok.Data + import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.stereotype.Component @@ -14,7 +14,6 @@ class MigrationConfigurationProperties { private PetriNetMigrationProperties petriNets = new PetriNetMigrationProperties() - @Data static class CaseMigrationProperties { private int pageSize = 100 @@ -24,7 +23,6 @@ class MigrationConfigurationProperties { } } - @Data static class TaskMigrationProperties { private int pageSize = 100 @@ -34,7 +32,6 @@ class MigrationConfigurationProperties { } } - @Data static class PetriNetMigrationProperties { private int pageSize = 100 diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy index 15d363a7e45..54e004cf9c5 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy @@ -34,14 +34,14 @@ abstract class AbstractMigrationHelper<T> { * It is expected to be provided by subclasses, as the class itself is generic and requires * specific document type initialization to perform the corresponding operations. */ - private Class<T> type + private final Class<T> type /** * The {@link MongoTemplate} used for interacting with the MongoDB database. * This is the core dependency of the helper class, allowing it to execute queries, * bulk operations, and other database operations on the specified document type. */ - private MongoTemplate mongoTemplate + private final MongoTemplate mongoTemplate /** * Constructs a new AbstractMigrationHelper with the specified MongoTemplate. @@ -86,6 +86,7 @@ abstract class AbstractMigrationHelper<T> { e.getWriteErrors().forEach { log.error("Error writing document with ID ${it.toString()}. Cause: ${it.getMessage()}") } + throw e } } @@ -113,14 +114,17 @@ abstract class AbstractMigrationHelper<T> { */ void iterate(Closure update, Closure processOperations = DEFAULT_PROCESS_OPERATIONS, Query query = new Query(), long sleepFor = 0, int pageSize = getPageSize()) { + if (pageSize <= 0) { + throw new IllegalArgumentException("pageSize must be > 0") + } long count = mongoTemplate.count(query, type) if (count > 0) { - long numOfPages = ((count / pageSize) + 1) as long + long numOfPages = Math.ceil(count / pageSize) as long log.info("Processing ${type.getSimpleName()} documents with filter ${query.toString()}: $numOfPages pages") long page = 1, currentBatchSize = 0 query.cursorBatchSize(pageSize) - BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, Case.class) + BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, type) try (CloseableIterator<T> cursor = mongoTemplate.stream(query, type)) { while (cursor.hasNext()) { @@ -128,7 +132,7 @@ abstract class AbstractMigrationHelper<T> { if (++currentBatchSize == pageSize as long || !cursor.hasNext()) { log.info("Processed ${type.getSimpleName()} document page {} / {}", page, numOfPages) processOperations(bulkOps) - bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, Case.class) + bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, type) currentBatchSize = 0 page++ if (sleepFor > 0) { diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index d2b943af8f2..48a01013045 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -13,6 +13,7 @@ import com.netgrif.application.engine.workflow.domain.Case import com.netgrif.application.engine.workflow.domain.DataField import com.querydsl.core.types.Predicate import groovy.util.logging.Slf4j +import org.junit.Assert import org.springframework.data.mongodb.core.BulkOperations import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Criteria @@ -37,27 +38,27 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { /** * Configuration properties for case migration. */ - private CaseMigrationProperties caseMigrationProperties + private final CaseMigrationProperties caseMigrationProperties /** * Service for managing PetriNet operations. */ - private IPetriNetService petriNetService + private final IPetriNetService petriNetService /** * Service for indexing and managing cases in Elasticsearch. */ - private IElasticCaseService elasticCaseService + private final IElasticCaseService elasticCaseService /** * Service for mapping Case objects to Elasticsearch documents. */ - private IElasticCaseMappingService elasticCaseMappingService + private final IElasticCaseMappingService elasticCaseMappingService /** * Helper for managing task migrations associated with cases. */ - private TaskMigrationHelper taskMigrationHelper + private final TaskMigrationHelper taskMigrationHelper /** * Constructs a CaseMigrationHelper instance with @@ -136,9 +137,9 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * * @param update A closure containing the code to execute for each matching case. * @param processIdentifier The identifier of the PetriNet process. - * @param pageSize Optional page size for processing cases. Default is 100.0. + * @param pageSize Optional page size for processing cases. Default is 100. */ - void updateCasesCursor(Closure update, String processIdentifier, double pageSize = 100.0) { + void updateCasesCursor(Closure update, String processIdentifier, int pageSize = 100) { Query query = new Query(Criteria.where("processIdentifier").is(processIdentifier)) iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int) } @@ -147,9 +148,9 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * Updates all cases in the system. The update closure is executed for each case. * * @param update A closure containing the code to execute for each case. - * @param pageSize Optional page size for processing cases. Default is 100.0. + * @param pageSize Optional page size for processing cases. Default is 100. */ - void updateAllCasesCursor(Closure update, double pageSize = 100.0) { + void updateAllCasesCursor(Closure update, int pageSize = 100) { iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int) } @@ -160,14 +161,21 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { */ void elasticIndex(Case useCase) { try { - PetriNetMigrationHelper.setPetriNet(useCase, petriNetService.getNewestVersionByIdentifier(useCase.processIdentifier)) - assert useCase.petriNet + PetriNetMigrationHelper.setPetriNet(useCase, petriNetService.get(useCase.petriNetObjectId)) + if (!useCase.petriNet) { + log.error("Failed to set petriNet for case $useCase.stringId") + return + } elasticCaseService.indexNow(elasticCaseMappingService.transform(useCase)) } catch (Exception ex) { if (useCase.lastModified == null) { - log.error("Creating new lastModified date for $useCase.stringId") + log.warn("Creating new lastModified date for $useCase.stringId") useCase.lastModified = LocalDateTime.now() - elasticCaseService.indexNow(elasticCaseMappingService.transform(useCase)) + try { + elasticCaseService.indexNow(elasticCaseMappingService.transform(useCase)) + } catch (Exception retryEx) { + log.error("Failed to index $useCase.stringId after setting lastModified", retryEx) + } } else { log.error("Failed to index $useCase.stringId", ex) } @@ -179,7 +187,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toDelete List of field IDs that will be deleted from useCase */ - static void deleteDataFields(Case useCase, List<String> toDelete) { + static void deleteDataFields(Case useCase, Set<String> toDelete) { toDelete.each { dataFieldID -> useCase.dataSet.remove(dataFieldID) } @@ -190,11 +198,12 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - static void changeDataFieldsValueFromNumberToText(Case useCase, List<String> toChange) { + static void changeDataFieldsValueFromNumberToText(Case useCase, Set<String> toChange) { toChange.each { dataFieldID -> - if (useCase.dataSet[dataFieldID].value && (useCase.dataSet[dataFieldID].value != null || useCase.dataSet[dataFieldID].value != "")) { - double value = useCase.dataSet[dataFieldID].value as double - useCase.dataSet[dataFieldID].value = value as String + DataField dataField = useCase.dataSet[dataFieldID] + if (dataField?.value != null && dataField.value != "") { + double value = dataField.value as double + dataField.value = value as String } } } @@ -204,14 +213,16 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - static void changeDataFieldsValueFromTextToNumber(Case useCase, List<String> toChange) { + static void changeDataFieldsValueFromTextToNumber(Case useCase, Set<String> toChange) { toChange.each { dataFieldID -> - if (useCase.dataSet[dataFieldID].value && useCase.dataSet[dataFieldID].value != "") { + DataField dataField = useCase.dataSet[dataFieldID] + if (dataField.value && dataField.value != "") { try { - useCase.dataSet[dataFieldID].value = useCase.dataSet[dataFieldID].value as double + dataField.value = dataField.value as double } catch (Exception e) { - useCase.dataSet[dataFieldID].value = null - log.error("[${useCase.stringId}] could not convert value ${useCase.dataSet[dataFieldID].value} in field ${dataFieldID}", e) + def originalValue = dataField.value + dataField.value = null + log.error("[${useCase.stringId}] could not convert value ${originalValue} in field ${dataFieldID}", e) } } } @@ -233,19 +244,20 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - static void changeDataFieldsValueFromEnumerationToMultichoice(Case useCase, List<String> toChange) { + static void changeDataFieldsValueFromEnumerationToMultichoice(Case useCase, Set<String> toChange) { toChange.each { dataFieldID -> - if (useCase.dataSet[dataFieldID].value && useCase.dataSet[dataFieldID].value != null) { + DataField dataField = useCase.dataSet[dataFieldID] + if (dataField.value && dataField.value != null) { def value - if (useCase.dataSet[dataFieldID].value instanceof I18nString) { - value = useCase.dataSet[dataFieldID].value as I18nString + if (dataField.value instanceof I18nString) { + value = dataField.value as I18nString } else { - value = new I18nString(useCase.dataSet[dataFieldID].value as String) + value = new I18nString(dataField.value as String) } def newSet = new HashSet<I18nString>() newSet.add(value) - useCase.dataSet[dataFieldID].value = newSet + dataField.value = newSet } } } @@ -257,12 +269,13 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { */ static void addChoices(Case useCase, Map<String, List<String>> toAdd) { toAdd.each { dataFieldID, newChoices -> - if (useCase.dataSet[dataFieldID].choices == null) { - useCase.dataSet[dataFieldID].setChoices(new HashSet<I18nString>()) + DataField dataField = useCase.dataSet[dataFieldID] + if (dataField.choices == null) { + dataField.setChoices(new HashSet<I18nString>()) } newChoices.each { - useCase.dataSet[dataFieldID].choices.add(new I18nString(it)) + dataField.choices.add(new I18nString(it)) } } } @@ -274,12 +287,13 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { */ static void removeChoices(Case useCase, Map<String, List<String>> toRemove) { toRemove.each { dataFieldID, choicesToRemove -> - if (useCase.dataSet[dataFieldID].value != null) { - (useCase.dataSet[dataFieldID].value as Set).removeAll(choicesToRemove) + DataField dataField = useCase.dataSet[dataFieldID] + if (dataField.value != null) { + (dataField.value as Set).removeAll(choicesToRemove) } - if (useCase.dataSet[dataFieldID].choices != null) { - useCase.dataSet[dataFieldID].choices.removeAll(choicesToRemove) + if (dataField.choices != null) { + dataField.choices.removeAll(choicesToRemove) } } } @@ -291,8 +305,12 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { */ static void changeFileFieldToFileList(Case useCase, String fieldId) { FileListFieldValue fileListFieldValue = new FileListFieldValue() - fileListFieldValue.namesPaths.add(useCase.dataSet[fieldId].value as FileFieldValue) - useCase.dataSet[fieldId].value = fileListFieldValue + DataField dataField = useCase.dataSet[fieldId] + def existingValue = dataField?.value + if (existingValue != null) { + fileListFieldValue.namesPaths.add(existingValue as FileFieldValue) + } + dataField.value = fileListFieldValue } /** @@ -319,7 +337,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ - static void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false) { + void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false) { useCase.permissions = net.getPermissions().entrySet().stream() .filter(role -> role.getValue().containsKey("delete") || role.getValue().containsKey("view")) .map(role -> { diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy index 4f51f9295b6..c287b54085a 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy @@ -1,6 +1,6 @@ package com.netgrif.application.engine.migration.helpers - +import com.netgrif.application.engine.auth.service.UserService import com.netgrif.application.engine.importer.service.Importer import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.PetriNetMigrationProperties @@ -57,23 +57,28 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * Configuration properties specific to Petri Net migration operations. * Contains settings such as page size and other Petri Net-related migration configurations. */ - private PetriNetMigrationProperties petriNetMigrationProperties + private final PetriNetMigrationProperties petriNetMigrationProperties /** * Service interface for managing Petri Net operations including importing, saving, and retrieving Petri Net models. */ - private IPetriNetService petriNetService + private final IPetriNetService petriNetService /** * Repository for persisting and retrieving process roles from the database. */ - private ProcessRoleRepository processRoleRepository + private final ProcessRoleRepository processRoleRepository /** * Provider that supplies {@link Importer} instances for importing Petri Net models from various sources. * Uses lazy initialization to create Importer instances on demand. */ - private Provider<Importer> importerProvider + private final Provider<Importer> importerProvider + + /** + * Service for managing user-related operations, including retrieving system user for Petri Net imports. + */ + private final UserService userService /** * Constructs a new PetriNetMigrationHelper with the specified dependencies. @@ -83,17 +88,20 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param petriNetService the {@link IPetriNetService} for managing Petri Net operations such as importing, saving, and retrieving Petri Nets * @param processRoleRepository the {@link ProcessRoleRepository} for persisting and retrieving process roles from the database * @param importerProvider the {@link Provider} that supplies {@link Importer} instances for importing Petri Net models from various sources + * @param userService the {@link UserService} for managing user-related operations, including retrieving system user for Petri Net imports */ PetriNetMigrationHelper(MongoTemplate mongoTemplate, MigrationConfigurationProperties migrationConfigurationProperties, IPetriNetService petriNetService, ProcessRoleRepository processRoleRepository, - Provider<Importer> importerProvider) { + Provider<Importer> importerProvider, + UserService userService) { super(PetriNet.class, mongoTemplate) this.petriNetMigrationProperties = migrationConfigurationProperties.petriNets this.petriNetService = petriNetService this.processRoleRepository = processRoleRepository this.importerProvider = importerProvider + this.userService = userService } /** @@ -115,8 +123,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { */ @Override void prepareOperations(PetriNet document, Closure update, BulkOperations bulkOperations) { - log.debug("Updating case with ID ${document.stringId}") - log.trace("Updating case ${document.toString()}") + log.debug("Updating Petri Net with ID ${document.stringId}") update(document) bulkOperations.replaceOne(Query.query(Criteria.where("_id").is(document.getObjectId())), document) } @@ -127,7 +134,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param resource Resource object with new version of Petri Net model */ void updateNetIgnoreRoles(String identifier, Resource resource, List<Closure<PetriNet>> customUpdates = null) { - PetriNet reimported = petriNetService.importPetriNet(resource.inputStream, VersionType.MAJOR, userService.system.transformToLoggedUser()).getNet() + PetriNet reimported = petriNetService.importPetriNet(resource.inputStream, VersionType.MAJOR, userService.getSystem().transformToLoggedUser()).getNet() updateNetIgnoreRoles(petriNetService.getNewestVersionByIdentifier(identifier), reimported, customUpdates) } @@ -141,7 +148,8 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { InputStream inputStream = new ClassPathResource("petriNets/$fileName" as String).inputStream ByteArrayOutputStream outputStream = new ByteArrayOutputStream() IOUtils.copy(inputStream, outputStream) - PetriNet reimported = getImporter().importPetriNet(new ByteArrayInputStream(outputStream.toByteArray())).get() + PetriNet reimported = getImporter().importPetriNet(new ByteArrayInputStream(outputStream.toByteArray())) + .orElseThrow { new IllegalStateException("Failed to import Petri Net from file: $fileName") } updateNetIgnoreRoles(currentNet, reimported, customUpdates) } @@ -236,13 +244,13 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * Helper method used in updateNetIgnoreRoles method, it sorts PetriNet dataSet alphabetically * @param petriNet Instance of Petri Net */ - static void resolveDataOrder(PetriNet petriNet) { - Collator skCollator = Collator.getInstance(new Locale("sk", "SK")) + static void resolveDataOrder(PetriNet petriNet, Locale locale = Locale.getDefault()) { + Collator collator = Collator.getInstance(locale) List<Field> fields = new LinkedList<>(petriNet.getDataSet().values()) fields = fields.stream().sorted({ f1, f2 -> int comparedTypes = f2.type.name <=> f1.type.name if (comparedTypes != 0) return comparedTypes - return skCollator.compare((f1.name?.defaultValue ?: f1.stringId), (f2.name?.defaultValue ?: f2.stringId)) + return collator.compare((f1.name?.defaultValue ?: f1.stringId), (f2.name?.defaultValue ?: f2.stringId)) }).collect(Collectors.toList()) petriNet.dataSet = fields.collectEntries { [(it.getStringId()): (it)] } as LinkedHashMap<String, Field> } @@ -256,6 +264,10 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { */ static void updateTransitionRoles(PetriNet net, String transitionId, ProcessRole role, Map<String, Boolean> permissions) { Transition trans = net.transitions.values().find { it.importId == transitionId } + if (!trans) { + log.warn("Transition with importId $transitionId not found in net $net.identifier") + return + } trans.roles[role.stringId] = permissions } @@ -268,6 +280,10 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { */ static void updateTransitionRoles(PetriNet net, String transitionId, String roleImportId, Map<String, Boolean> permissions) { ProcessRole role = net.roles.values().find { it.importId == roleImportId } + if (!role) { + log.warn("Transition with importId $transitionId not found in net $net.identifier") + return + } updateTransitionRoles(net, transitionId, role, permissions) } @@ -291,7 +307,9 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { */ void updateDataSet(String identifier, String fileName, Closure<PetriNet> customUpdate = null) { PetriNet existing = petriNetService.getNewestVersionByIdentifier(identifier) - PetriNet reimported = getImporter().importPetriNet(new File("src/main/resources/petriNets/" + fileName)).get() + InputStream inputStream = new ClassPathResource("petriNets/$fileName" as String).inputStream + PetriNet reimported = getImporter().importPetriNet(inputStream) + .orElseThrow { new IllegalStateException("Failed to import Petri Net from file: $fileName") } reimported = replaceUserFieldRoleReferences(existing, reimported) @@ -420,6 +438,10 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { newRoles.each { newRole -> ProcessRole role = oldRoles.find { it.importId == newRole.importId } + if (!role) { + log.warn("No existing role found for importId $newRole.importId, skipping event update") + return + } role.events = newRole.events processRoleRepository.save(role) } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy index 915e19ea8bb..e0721ccc660 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy @@ -38,7 +38,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * such as the size of the page used to process tasks in the migration. * It is loaded from the {@link MigrationConfigurationProperties} during initialization. */ - private TaskMigrationProperties taskMigrationProperties + private final TaskMigrationProperties taskMigrationProperties /** * Service for handling Petri Net operations. @@ -47,7 +47,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * such as retrieving the latest version of a Petri Net by its identifier * during task migrations. */ - private IPetriNetService petriNetService + private final IPetriNetService petriNetService /** * Service for handling task operations. @@ -55,7 +55,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * This service provides methods for managing task entities, * including finding, saving, and reloading tasks during migration processes. */ - private ITaskService taskService + private final ITaskService taskService /** * Service for handling Elasticsearch task indexing operations. @@ -63,7 +63,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * This service is used to index task documents into Elasticsearch, * enabling full-text search and analytics capabilities for tasks. */ - private IElasticTaskService elasticTaskService + private final IElasticTaskService elasticTaskService /** * Service for mapping task entities to Elasticsearch documents. @@ -71,7 +71,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * This service transforms task domain objects into their Elasticsearch * representation before indexing, ensuring proper field mapping and data structure. */ - private IElasticTaskMappingService elasticTaskMappingService + private final IElasticTaskMappingService elasticTaskMappingService /** * Constructs a new TaskMigrationHelper with the specified MongoTemplate. @@ -118,8 +118,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { */ @Override void prepareOperations(Task document, Closure update, BulkOperations bulkOperations) { - log.debug("Updating case with ID ${document.stringId}") - log.trace("Updating case ${document.toString()}") + log.debug("Updating task with ID ${document.stringId}") update(document) bulkOperations.replaceOne(Query.query(Criteria.where("_id").is(document.getObjectId())), document) } @@ -239,7 +238,6 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { Transition newTransition = net.getTransition(taskPair.transition) Task oldTask = taskService.findOne(taskPair.task) oldTask.setProcessId(net.stringId) - oldTask.getRoles().clear() oldTask.setRoles(newTransition.roles) oldTask.setNegativeViewRoles(newTransition.negativeViewRoles) oldTask.resolveViewRoles() diff --git a/src/test/groovy/com/netgrif/application/engine/migration/MigrationBenchmarkTest.groovy b/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy similarity index 77% rename from src/test/groovy/com/netgrif/application/engine/migration/MigrationBenchmarkTest.groovy rename to src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy index 75194cb8c49..a92c2e80a4a 100644 --- a/src/test/groovy/com/netgrif/application/engine/migration/MigrationBenchmarkTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy @@ -12,8 +12,6 @@ import com.netgrif.application.engine.workflow.domain.QCase import com.netgrif.application.engine.workflow.domain.eventoutcomes.petrinetoutcomes.ImportPetriNetEventOutcome import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService import groovy.util.logging.Slf4j -import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith @@ -23,14 +21,11 @@ import org.springframework.data.domain.Pageable; import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.junit.jupiter.SpringExtension -import java.time.Duration -import java.time.LocalDateTime - @Slf4j @SpringBootTest @ActiveProfiles(["test"]) @ExtendWith(SpringExtension.class) -class MigrationBenchmarkTest { +class MigrationTest { @Autowired private TestHelper testHelper @@ -52,28 +47,30 @@ class MigrationBenchmarkTest { private PetriNet netV1, netV2 - private static FileWriter writer - @BeforeEach void beforeEach() { testHelper.truncateDbs() - ImportPetriNetEventOutcome netV1Outcome = petriNetService.importPetriNet(new FileInputStream("src/test/resources/nae_2432_v1.xml"), VersionType.MAJOR, superCreator.getLoggedSuper()) - assert netV1Outcome.getNet() != null - netV1 = netV1Outcome.getNet() + new FileInputStream("src/test/resources/nae_2432_v1.xml").withCloseable { is -> + ImportPetriNetEventOutcome netV1Outcome = petriNetService.importPetriNet(is, VersionType.MAJOR, superCreator.getLoggedSuper()) + assert netV1Outcome.getNet() != null + netV1 = netV1Outcome.getNet() + } - ImportPetriNetEventOutcome netV2Outcome = petriNetService.importPetriNet(new FileInputStream("src/test/resources/nae_2432_v2.xml"), VersionType.MAJOR, superCreator.getLoggedSuper()) - assert netV2Outcome.getNet() != null - netV2 = netV2Outcome.getNet() + new FileInputStream("src/test/resources/nae_2432_v2.xml").withCloseable { is -> + ImportPetriNetEventOutcome netV2Outcome = petriNetService.importPetriNet(is, VersionType.MAJOR, superCreator.getLoggedSuper()) + assert netV2Outcome.getNet() != null + netV2 = netV2Outcome.getNet() + } - (1..100).stream().parallel().forEach { + (1..10).stream().parallel().forEach { workflowService.createCase(netV1.stringId, "Net V1 " + it, null, superCreator.loggedSuper, Locale.default) } } @Test void migrateCasesWithCursor() { - List<Case> caseList = workflowService.search(QCase.case$.processIdentifier.eq("nae_2432"), Pageable.ofSize(100)).getContent() + List<Case> caseList = workflowService.search(QCase.case$.processIdentifier.eq("nae_2432"), Pageable.ofSize(10)).getContent() caseList.forEach { assert !it.dataSet.containsKey("income") assert !it.dataSet.containsKey("recreate_info_text") @@ -90,7 +87,7 @@ class MigrationBenchmarkTest { useCase.dataSet["recreate_info_text"] = new DataField("") }, "nae_2432") - caseList = workflowService.search(QCase.case$.processIdentifier.eq("nae_2432"), Pageable.ofSize(100)).getContent() + caseList = workflowService.search(QCase.case$.processIdentifier.eq("nae_2432"), Pageable.ofSize(10)).getContent() caseList.forEach { assert it.dataSet.containsKey("income") assert it.dataSet.containsKey("recreate_info_text") From a1f15a549d99fd8020050be308fc9f1f0bc0b318 Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Thu, 28 May 2026 10:55:20 +0200 Subject: [PATCH 08/20] [NAE-2433] Implement Migration Helper Integration for Engine 6.x - changed properties to protected --- .../migration/helpers/CaseMigrationHelper.groovy | 10 +++++----- .../migration/helpers/PetriNetMigrationHelper.groovy | 11 +++++------ .../migration/helpers/TaskMigrationHelper.groovy | 10 +++++----- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index 48a01013045..6c46bdc6d42 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -38,27 +38,27 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { /** * Configuration properties for case migration. */ - private final CaseMigrationProperties caseMigrationProperties + protected final CaseMigrationProperties caseMigrationProperties /** * Service for managing PetriNet operations. */ - private final IPetriNetService petriNetService + protected final IPetriNetService petriNetService /** * Service for indexing and managing cases in Elasticsearch. */ - private final IElasticCaseService elasticCaseService + protected final IElasticCaseService elasticCaseService /** * Service for mapping Case objects to Elasticsearch documents. */ - private final IElasticCaseMappingService elasticCaseMappingService + protected final IElasticCaseMappingService elasticCaseMappingService /** * Helper for managing task migrations associated with cases. */ - private final TaskMigrationHelper taskMigrationHelper + protected final TaskMigrationHelper taskMigrationHelper /** * Constructs a CaseMigrationHelper instance with diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy index c287b54085a..c349ac0fc70 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy @@ -52,33 +52,32 @@ import java.util.stream.Collectors @Component class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { - /** * Configuration properties specific to Petri Net migration operations. * Contains settings such as page size and other Petri Net-related migration configurations. */ - private final PetriNetMigrationProperties petriNetMigrationProperties + protected final PetriNetMigrationProperties petriNetMigrationProperties /** * Service interface for managing Petri Net operations including importing, saving, and retrieving Petri Net models. */ - private final IPetriNetService petriNetService + protected final IPetriNetService petriNetService /** * Repository for persisting and retrieving process roles from the database. */ - private final ProcessRoleRepository processRoleRepository + protected final ProcessRoleRepository processRoleRepository /** * Provider that supplies {@link Importer} instances for importing Petri Net models from various sources. * Uses lazy initialization to create Importer instances on demand. */ - private final Provider<Importer> importerProvider + protected final Provider<Importer> importerProvider /** * Service for managing user-related operations, including retrieving system user for Petri Net imports. */ - private final UserService userService + protected final UserService userService /** * Constructs a new PetriNetMigrationHelper with the specified dependencies. diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy index e0721ccc660..89f8a0184e5 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy @@ -38,7 +38,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * such as the size of the page used to process tasks in the migration. * It is loaded from the {@link MigrationConfigurationProperties} during initialization. */ - private final TaskMigrationProperties taskMigrationProperties + protected final TaskMigrationProperties taskMigrationProperties /** * Service for handling Petri Net operations. @@ -47,7 +47,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * such as retrieving the latest version of a Petri Net by its identifier * during task migrations. */ - private final IPetriNetService petriNetService + protected final IPetriNetService petriNetService /** * Service for handling task operations. @@ -55,7 +55,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * This service provides methods for managing task entities, * including finding, saving, and reloading tasks during migration processes. */ - private final ITaskService taskService + protected final ITaskService taskService /** * Service for handling Elasticsearch task indexing operations. @@ -63,7 +63,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * This service is used to index task documents into Elasticsearch, * enabling full-text search and analytics capabilities for tasks. */ - private final IElasticTaskService elasticTaskService + protected final IElasticTaskService elasticTaskService /** * Service for mapping task entities to Elasticsearch documents. @@ -71,7 +71,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * This service transforms task domain objects into their Elasticsearch * representation before indexing, ensuring proper field mapping and data structure. */ - private final IElasticTaskMappingService elasticTaskMappingService + protected final IElasticTaskMappingService elasticTaskMappingService /** * Constructs a new TaskMigrationHelper with the specified MongoTemplate. From c406aa7acc491fb164f1187147a64c7e59ddd9c5 Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Thu, 28 May 2026 10:57:08 +0200 Subject: [PATCH 09/20] [NAE-2433] Implement Migration Helper Integration for Engine 6.x - Improve bulk write error logging in AbstractMigrationHelper --- .../engine/migration/helpers/AbstractMigrationHelper.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy index 54e004cf9c5..e1398fa9fe1 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy @@ -82,7 +82,7 @@ abstract class AbstractMigrationHelper<T> { BulkWriteResult bulkWriteResult = bulkOps.execute() log.debug("Processed bulk write of ${bulkWriteResult.modifiedCount}") } catch (BulkWriteException e) { - log.error("Failed to write bulk operation", e.getMessage()) + log.error("Failed to write bulk operation", e) e.getWriteErrors().forEach { log.error("Error writing document with ID ${it.toString()}. Cause: ${it.getMessage()}") } From e2a0b7c8506a754aeaf859f96ec6dd399722f497 Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Thu, 28 May 2026 11:02:36 +0200 Subject: [PATCH 10/20] [NAE-2433] Implement Migration Helper Integration for Engine 6.x - Remove redundant trace log from `CaseMigrationHelper` --- .../engine/migration/helpers/CaseMigrationHelper.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index 6c46bdc6d42..62269513468 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -102,7 +102,6 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { @Override void prepareOperations(Case useCase, Closure update, BulkOperations bulkOperations) { log.debug("Updating case with ID ${useCase.stringId}") - log.trace("Updating case ${useCase.toString()}") update(useCase) bulkOperations.replaceOne(Query.query(Criteria.where("_id").is(useCase.get_id())), useCase) } From b5b7fe75c85aaa78587a9e1c3d506f32cc0bcd7d Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Thu, 28 May 2026 11:39:04 +0200 Subject: [PATCH 11/20] [NAE-2433] Implement Migration Helper Integration for Engine 6.x - Introduced debug and trace logs across `MigrationHelper`, `CaseMigrationHelper`, `TaskMigrationHelper`, and `PetriNetMigrationHelper`. - Logs include method parameters, processing state, and context to enhance debugging and analysis during migrations. --- .../engine/migration/MigrationHelper.groovy | 47 +++++++++++++++---- .../helpers/AbstractMigrationHelper.groovy | 2 +- .../helpers/CaseMigrationHelper.groovy | 33 ++++++++++++- .../helpers/PetriNetMigrationHelper.groovy | 31 ++++++++++-- .../helpers/TaskMigrationHelper.groovy | 33 ++++++++++--- 5 files changed, 123 insertions(+), 23 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy index 47a114d18ea..15fe90e1df0 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy @@ -73,6 +73,7 @@ class MigrationHelper { * @param filter Instance of Predicate, to filter which cases should be updated */ void updateCases(Closure update, Predicate filter) { + log.debug("updateCases called with filter: {}", filter) caseMigrationHelper.updateCases(update, filter) } @@ -83,6 +84,7 @@ class MigrationHelper { * @param filter Instance of Predicate, to filter which cases should be iterated */ void iterateCases(Closure update, Closure pageProcessed = AbstractMigrationHelper.DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { + log.debug("iterateCases called with filter: {}, sleepFor: {}", filter, sleepFor) caseMigrationHelper.iterateCases(update, pageProcessed, sleepFor, filter) } @@ -90,18 +92,20 @@ class MigrationHelper { * Updates all cases of a given process. * @param update Instance of Closure, which should contain code that will be executed for every Case matched by filter * @param processIdentifier identifier of PetriNet, to filter which cases should be updated - * @param pageSize Optional attribute to set page size. Default page size 100.0 + * @param pageSize Optional attribute to set page size. Default page size 100 */ - void updateCasesCursor(Closure update, String processIdentifier, double pageSize = 100.0) { + void updateCasesCursor(Closure update, String processIdentifier, int pageSize = 100) { + log.debug("updateCasesCursor called with processIdentifier: {}, pageSize: {}", processIdentifier, pageSize) caseMigrationHelper.updateCasesCursor(update, processIdentifier, pageSize) } /** * Update all cases. * @param update Instance of Closure, which should contain code that will be executed for every Case - * @param pageSize Optional attribute to set page size. Default page size 100.0 + * @param pageSize Optional attribute to set page size. Default page size 100 */ - void updateAllCasesCursor(Closure update, double pageSize = 100.0) { + void updateAllCasesCursor(Closure update, int pageSize = 100) { + log.debug("updateAllCasesCursor called with pageSize: {}", pageSize) caseMigrationHelper.updateAllCasesCursor(update, pageSize) } @@ -111,6 +115,7 @@ class MigrationHelper { * @param filter Instance of Predicate, to filter which tasks should be updated */ void updateTasks(Closure update, Predicate filter) { + log.debug("updateTasks called with filter: {}", filter) taskMigrationHelper.updateTasks(update, filter) } @@ -121,6 +126,7 @@ class MigrationHelper { * @param filter Instance of Predicate, to filter which tasks should be iterated */ void iterateTasks(Closure update, Closure pageProcessed = AbstractMigrationHelper.DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { + log.debug("iterateTasks called with filter: {}, sleepFor: {}", filter, sleepFor) taskMigrationHelper.iterateTasks(update, pageProcessed, sleepFor, filter) } @@ -128,9 +134,10 @@ class MigrationHelper { * Updates all tasks of a given process. * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter * @param processIdentifier identifier of PetriNet, to filter which tasks should be updated - * @param pageSize Optional attribute to set page size. Default page size 100.0 + * @param pageSize Optional attribute to set page size. Default page size 100 */ - void updateTasksCursor(Closure update, String processIdentifier, double pageSize = 100.0) { + void updateTasksCursor(Closure update, String processIdentifier, int pageSize = 100) { + log.debug("updateTasksCursor called with processIdentifier: {}, pageSize: {}", processIdentifier, pageSize) taskMigrationHelper.updateTasksCursor(update, processIdentifier, pageSize) } @@ -139,18 +146,20 @@ class MigrationHelper { * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter * @param processIdentifier identifier of PetriNet, to filter which tasks should be updated * @param transitionIds List of transition IDs to limit filter to specific transitions of given processIdentifier - * @param pageSize Optional attribute to set page size. Default page size 100.0 + * @param pageSize Optional attribute to set page size. Default page size 100 */ - void updateSpecificTasksCursor(Closure update, String processIdentifier, List<String> transitionIds, double pageSize = 100.0) { + void updateSpecificTasksCursor(Closure update, String processIdentifier, List<String> transitionIds, int pageSize = 100) { + log.debug("updateSpecificTasksCursor called with processIdentifier: {}, transitionIds: {}, pageSize: {}", processIdentifier, transitionIds, pageSize) taskMigrationHelper.updateSpecificTasksCursor(update, processIdentifier, transitionIds, pageSize) } /** * Update all tasks. * @param update Instance of Closure, which should contain code that will be executed for every Task - * @param pageSize Optional attribute to set page size. Default page size 100.0 + * @param pageSize Optional attribute to set page size. Default page size 100 */ - void updateAllTasksCursor(Closure update, double pageSize = 100.0) { + void updateAllTasksCursor(Closure update, int pageSize = 100) { + log.debug("updateAllTasksCursor called with pageSize: {}", pageSize) taskMigrationHelper.updateAllTasksCursor(update, pageSize) } @@ -160,6 +169,7 @@ class MigrationHelper { * @param resource Resource object with new version of Petri Net model */ void updateNetIgnoreRoles(String identifier, Resource resource, List<Closure<PetriNet>> customUpdates = null) { + log.debug("updateNetIgnoreRoles called with identifier: {}, resource: {}", identifier, resource) petriNetMigrationHelper.updateNetIgnoreRoles(identifier, resource, customUpdates) } @@ -169,6 +179,7 @@ class MigrationHelper { * @param fileName File name of new version of Petri Net model */ void updateNetIgnoreRoles(String identifier, String fileName, List<Closure<PetriNet>> customUpdates = null) { + log.debug("updateNetIgnoreRoles called with identifier: {}, fileName: {}", identifier, fileName) petriNetMigrationHelper.updateNetIgnoreRoles(identifier, fileName, customUpdates) } @@ -178,6 +189,7 @@ class MigrationHelper { * @param reimported New version of Petri Net object, its values will be applied to currentNet */ void updateNetIgnoreRoles(PetriNet currentNet, PetriNet reimported, List<Closure<PetriNet>> customUpdates) { + log.debug("updateNetIgnoreRoles called with currentNet: {}, reimported: {}", currentNet?.identifier, reimported?.identifier) petriNetMigrationHelper.updateNetIgnoreRoles(currentNet, reimported, customUpdates) } @@ -189,6 +201,7 @@ class MigrationHelper { * @param permissions New role permissions on transition */ void updateTransitionRoles(PetriNet net, String transitionId, ProcessRole role, Map<String, Boolean> permissions) { + log.debug("updateTransitionRoles called with net: {}, transitionId: {}, role: {}, permissions: {}", net?.identifier, transitionId, role?.stringId, permissions) petriNetMigrationHelper.updateTransitionRoles(net, transitionId, role, permissions) } @@ -200,6 +213,7 @@ class MigrationHelper { * @param permissions New role permissions on transition */ void updateTransitionRoles(PetriNet net, String transitionId, String roleImportId, Map<String, Boolean> permissions) { + log.debug("updateTransitionRoles called with net: {}, transitionId: {}, roleImportId: {}, permissions: {}", net?.identifier, transitionId, roleImportId, permissions) petriNetMigrationHelper.updateTransitionRoles(net, transitionId, roleImportId, permissions) } @@ -210,6 +224,7 @@ class MigrationHelper { * @param permissions New role permissions on transition */ Closure<PetriNet> updateTransitionRolesClosure(String transitionId, String roleImportId, Map<String, Boolean> permissions) { + log.debug("updateTransitionRolesClosure called with transitionId: {}, roleImportId: {}, permissions: {}", transitionId, roleImportId, permissions) petriNetMigrationHelper.updateTransitionRolesClosure(transitionId, roleImportId, permissions) } @@ -219,6 +234,7 @@ class MigrationHelper { * @param fileName File name of new version of Petri Net model */ void updateDataSet(String identifier, String fileName, Closure<PetriNet> customUpdate = null) { + log.debug("updateDataSet called with identifier: {}, fileName: {}", identifier, fileName) petriNetMigrationHelper.updateDataSet(identifier, fileName, customUpdate) } @@ -229,6 +245,7 @@ class MigrationHelper { * @param title Title of the new Process Role */ def createRoleInNet(String identifier, String id, String title, Map<EventType, Event> events = [:]) { + log.debug("createRoleInNet called with identifier: {}, id: {}, title: {}", identifier, id, title) return petriNetMigrationHelper.createRoleInNet(identifier, id, title, events) } @@ -239,6 +256,7 @@ class MigrationHelper { * @param title Title of the new Process Role */ def createRoleInNet(String identifier, String id, I18nString title, Map<EventType, Event> events = [:]) { + log.debug("createRoleInNet called with identifier: {}, id: {}, title: {}", identifier, id, title) return petriNetMigrationHelper.createRoleInNet(identifier, id, title, events) } @@ -248,6 +266,7 @@ class MigrationHelper { * @param title Title of the new Process Role */ def createGlobalRole(String id, String title, Map<EventType, Event> events = [:]) { + log.debug("createGlobalRole called with id: {}, title: {}", id, title) return petriNetMigrationHelper.createGlobalRole(id, title, event) } @@ -257,6 +276,7 @@ class MigrationHelper { * @param title Title of the new Process Role */ def createGlobalRole(String id, I18nString title, Map<EventType, Event> events = [:]) { + log.debug("createGlobalRole called with id: {}, title: {}", id, title) return petriNetMigrationHelper.createGlobalRole(id, title, events) } @@ -267,6 +287,7 @@ class MigrationHelper { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ void reloadTasks(Case useCase, PetriNet net) { + log.debug("reloadTasks called with useCase: {}, net: {}", useCase?.stringId, net?.identifier) taskMigrationHelper.reloadTasks(useCase, net) } @@ -276,6 +297,7 @@ class MigrationHelper { * @param useCase Instance of Case that will be indexed into elasticsearch index */ void elasticIndex(Case useCase) { + log.debug("elasticIndex called with useCase: {}", useCase?.stringId) caseMigrationHelper.elasticIndex(useCase) } @@ -284,6 +306,7 @@ class MigrationHelper { * @param task Instance of Task that will be indexed into elasticsearch index */ void elasticTaskIndex(Task task) { + log.debug("elasticTaskIndex called with task: {}", task?.stringId) taskMigrationHelper.elasticTaskIndex(task) } @@ -295,6 +318,7 @@ class MigrationHelper { * @param permissions Map of permissions for the role */ void addRoleToExistingTasks(ProcessRole role, PetriNet net, List<String> transitionIds, Map<String, Boolean> permissions) { + log.debug("addRoleToExistingTasks called with role: {}, net: {}, transitionIds: {}, permissions: {}", role?.stringId, net?.identifier, transitionIds, permissions) taskMigrationHelper.addRoleToExistingTasks(role, net, transitionIds, permissions) } @@ -418,6 +442,7 @@ class MigrationHelper { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false) { + log.debug("updateCasePermissionsFromNet called with useCase: {}, net: {}, updateTasks: {}", useCase?.stringId, net?.identifier, updateTasks) caseMigrationHelper.updateCasePermissionsFromNet(useCase, net, updateTasks) } @@ -428,6 +453,7 @@ class MigrationHelper { * @param relevantTransitionIds List of transition IDs for permissions update */ void updateTasksPermissions(Case useCase, PetriNet net, List<String> relevantTransitionIds) { + log.debug("updateTasksPermissions called with useCase: {}, net: {}, relevantTransitionIds: {}", useCase?.stringId, net?.identifier, relevantTransitionIds) taskMigrationHelper.updateTasksPermissions(useCase, net, relevantTransitionIds) } @@ -438,6 +464,7 @@ class MigrationHelper { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ void updateTaskPermissions(Case useCase, TaskPair taskPair, PetriNet net) { + log.debug("updateTaskPermissions called with useCase: {}, taskPair: {}, net: {}", useCase?.stringId, taskPair?.task?.stringId, net?.identifier) taskMigrationHelper.updateTaskPermissions(useCase, taskPair, net) } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy index e1398fa9fe1..17e566a2880 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy @@ -130,7 +130,7 @@ abstract class AbstractMigrationHelper<T> { while (cursor.hasNext()) { prepareOperations(cursor.next(), update, bulkOps) if (++currentBatchSize == pageSize as long || !cursor.hasNext()) { - log.info("Processed ${type.getSimpleName()} document page {} / {}", page, numOfPages) + log.debug("Processed ${type.getSimpleName()} document page {} / {}", page, numOfPages) processOperations(bulkOps) bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, type) currentBatchSize = 0 diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index 62269513468..3cf83739946 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -114,7 +114,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param filter A QueryDSL Predicate object specifying the conditions to filter the cases. */ void updateCases(Closure update, Predicate filter) { - log.info("Updating cases with filter ${filter.toString()} and update ${update.toString()}") + log.debug("Updating cases with filter ${filter.toString()} and update ${update.toString()}") iterate(update, DEFAULT_PROCESS_OPERATIONS, toQuery(filter)) } @@ -128,6 +128,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param filter A QueryDSL Predicate object specifying the conditions to filter the cases. */ void iterateCases(Closure update, Closure pageProcessed = DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { + log.debug("Starting iterateCases with filter: ${filter.toString()}, sleepFor: ${sleepFor}ms") iterate(update, pageProcessed, toQuery(filter), sleepFor) } @@ -139,6 +140,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param pageSize Optional page size for processing cases. Default is 100. */ void updateCasesCursor(Closure update, String processIdentifier, int pageSize = 100) { + log.debug("Starting updateCasesCursor for processIdentifier: ${processIdentifier}, pageSize: ${pageSize}") Query query = new Query(Criteria.where("processIdentifier").is(processIdentifier)) iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int) } @@ -150,6 +152,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param pageSize Optional page size for processing cases. Default is 100. */ void updateAllCasesCursor(Closure update, int pageSize = 100) { + log.debug("Starting updateAllCasesCursor with pageSize: ${pageSize}") iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int) } @@ -159,12 +162,14 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case that will be indexed into elasticsearch index */ void elasticIndex(Case useCase) { + log.debug("Starting elasticIndex for case: ${useCase.stringId}") try { PetriNetMigrationHelper.setPetriNet(useCase, petriNetService.get(useCase.petriNetObjectId)) if (!useCase.petriNet) { log.error("Failed to set petriNet for case $useCase.stringId") return } + log.trace("Successfully set petriNet for case: ${useCase.stringId}") elasticCaseService.indexNow(elasticCaseMappingService.transform(useCase)) } catch (Exception ex) { if (useCase.lastModified == null) { @@ -187,7 +192,9 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param toDelete List of field IDs that will be deleted from useCase */ static void deleteDataFields(Case useCase, Set<String> toDelete) { + log.debug("Starting deleteDataFields for case: ${useCase.stringId}, fields to delete: ${toDelete}") toDelete.each { dataFieldID -> + log.trace("Removing data field: ${dataFieldID} from case: ${useCase.stringId}") useCase.dataSet.remove(dataFieldID) } } @@ -198,11 +205,13 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param toChange List of field IDs for value change */ static void changeDataFieldsValueFromNumberToText(Case useCase, Set<String> toChange) { + log.debug("Starting changeDataFieldsValueFromNumberToText for case: ${useCase.stringId}, fields to change: ${toChange}") toChange.each { dataFieldID -> DataField dataField = useCase.dataSet[dataFieldID] if (dataField?.value != null && dataField.value != "") { double value = dataField.value as double dataField.value = value as String + log.trace("Converted field ${dataFieldID} from number ${value} to text in case: ${useCase.stringId}") } } } @@ -213,11 +222,14 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param toChange List of field IDs for value change */ static void changeDataFieldsValueFromTextToNumber(Case useCase, Set<String> toChange) { + log.debug("Starting changeDataFieldsValueFromTextToNumber for case: ${useCase.stringId}, fields to change: ${toChange}") toChange.each { dataFieldID -> DataField dataField = useCase.dataSet[dataFieldID] if (dataField.value && dataField.value != "") { try { + def originalValue = dataField.value dataField.value = dataField.value as double + log.trace("Converted field ${dataFieldID} from text ${originalValue} to number in case: ${useCase.stringId}") } catch (Exception e) { def originalValue = dataField.value dataField.value = null @@ -233,7 +245,9 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param toAdd Map<field id, init value of field> */ static void addTextDataFields(Case useCase, Map<String, String> toAdd) { + log.debug("Starting addTextDataFields for case: ${useCase.stringId}, fields to add: ${toAdd.keySet()}") toAdd.each { dataFieldID, value -> + log.trace("Adding text data field ${dataFieldID} with value '${value}' to case: ${useCase.stringId}") useCase.dataSet[dataFieldID] = new DataField(value) } } @@ -244,6 +258,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param toChange List of field IDs for value change */ static void changeDataFieldsValueFromEnumerationToMultichoice(Case useCase, Set<String> toChange) { + log.debug("Starting changeDataFieldsValueFromEnumerationToMultichoice for case: ${useCase.stringId}, fields to change: ${toChange}") toChange.each { dataFieldID -> DataField dataField = useCase.dataSet[dataFieldID] if (dataField.value && dataField.value != null) { @@ -257,6 +272,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { def newSet = new HashSet<I18nString>() newSet.add(value) dataField.value = newSet + log.trace("Converted field ${dataFieldID} from enumeration to multichoice in case: ${useCase.stringId}") } } } @@ -267,6 +283,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param toAdd Map<field id, list of choices to add into data data field> */ static void addChoices(Case useCase, Map<String, List<String>> toAdd) { + log.debug("Starting addChoices for case: ${useCase.stringId}, fields: ${toAdd.keySet()}") toAdd.each { dataFieldID, newChoices -> DataField dataField = useCase.dataSet[dataFieldID] if (dataField.choices == null) { @@ -274,6 +291,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { } newChoices.each { + log.trace("Adding choice '${it}' to field ${dataFieldID} in case: ${useCase.stringId}") dataField.choices.add(new I18nString(it)) } } @@ -285,7 +303,9 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param toAdd Map<field id, list of choices to add into data field> */ static void removeChoices(Case useCase, Map<String, List<String>> toRemove) { + log.debug("Starting removeChoices for case: ${useCase.stringId}, fields: ${toRemove.keySet()}") toRemove.each { dataFieldID, choicesToRemove -> + log.trace("Removing choices ${choicesToRemove} from field ${dataFieldID} in case: ${useCase.stringId}") DataField dataField = useCase.dataSet[dataFieldID] if (dataField.value != null) { (dataField.value as Set).removeAll(choicesToRemove) @@ -303,6 +323,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param fieldId Field ID for value change */ static void changeFileFieldToFileList(Case useCase, String fieldId) { + log.debug("Starting changeFileFieldToFileList for case: ${useCase.stringId}, field: ${fieldId}") FileListFieldValue fileListFieldValue = new FileListFieldValue() DataField dataField = useCase.dataSet[fieldId] def existingValue = dataField?.value @@ -310,6 +331,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { fileListFieldValue.namesPaths.add(existingValue as FileFieldValue) } dataField.value = fileListFieldValue + log.trace("Converted field ${fieldId} from FileFieldValue to FileListFieldValue in case: ${useCase.stringId}") } /** @@ -318,14 +340,17 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ static void updateCaseComponents(Case useCase, PetriNet net) { + log.debug("Starting updateCaseComponents for case: ${useCase.stringId}, net: ${net.stringId}") Map<String, com.netgrif.application.engine.petrinet.domain.Component> components = PetriNetMigrationHelper.createComponentsMap(net) Map<String, Map<String, com.netgrif.application.engine.petrinet.domain.Component>> dataRefComponents = PetriNetMigrationHelper.createDataRefComponentsMap(net) - useCase.dataSet.each {dataField -> + useCase.dataSet.each { dataField -> if (components[dataField.key]) { + log.trace("Updating component for field ${dataField.key} in case: ${useCase.stringId}") useCase.dataSet[dataField.key].component = components[dataField.key] } if (dataRefComponents[dataField.key]) { + log.trace("Updating dataRef components for field ${dataField.key} in case: ${useCase.stringId}") useCase.dataSet[dataField.key].dataRefComponents = dataRefComponents[dataField.key] } } @@ -337,6 +362,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false) { + log.debug("Starting updateCasePermissionsFromNet for case: ${useCase.stringId}, net: ${net.stringId}, updateTasks: ${updateTasks}") useCase.permissions = net.getPermissions().entrySet().stream() .filter(role -> role.getValue().containsKey("delete") || role.getValue().containsKey("view")) .map(role -> { @@ -351,6 +377,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)) useCase.resolveViewRoles() useCase.setEnabledRoles(net.getRoles().keySet()) + log.trace("Updated permissions and enabled roles for case: ${useCase.stringId}") if (updateTasks) { useCase.tasks.each { taskPair -> taskMigrationHelper.updateTaskPermissions(useCase, taskPair, net) @@ -364,7 +391,9 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param newNet Instance of Petri Net, it needs to match processIdentifier of useCase */ static void migratePetriNet(Case useCase, PetriNet newNet) { + log.debug("Starting migratePetriNet for case: ${useCase.stringId}, new net: ${newNet.stringId}") useCase.setPetriNetObjectId(newNet.objectId) + log.trace("Updated petriNet reference for case: ${useCase.stringId} to net: ${newNet.stringId}") } } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy index c349ac0fc70..4e44b183f61 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy @@ -133,6 +133,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param resource Resource object with new version of Petri Net model */ void updateNetIgnoreRoles(String identifier, Resource resource, List<Closure<PetriNet>> customUpdates = null) { + log.debug("Starting updateNetIgnoreRoles for identifier: {} with Resource", identifier) PetriNet reimported = petriNetService.importPetriNet(resource.inputStream, VersionType.MAJOR, userService.getSystem().transformToLoggedUser()).getNet() updateNetIgnoreRoles(petriNetService.getNewestVersionByIdentifier(identifier), reimported, customUpdates) } @@ -143,6 +144,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param fileName File name of new version of Petri Net model */ void updateNetIgnoreRoles(String identifier, String fileName, List<Closure<PetriNet>> customUpdates = null) { + log.debug("Starting updateNetIgnoreRoles for identifier: {} with fileName: {}", identifier, fileName) PetriNet currentNet = petriNetService.getNewestVersionByIdentifier(identifier) InputStream inputStream = new ClassPathResource("petriNets/$fileName" as String).inputStream ByteArrayOutputStream outputStream = new ByteArrayOutputStream() @@ -159,6 +161,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param customUpdates Optional list of custom update closures to be applied after the standard update */ void updateNetIgnoreRoles(PetriNet currentNet, PetriNet reimported, List<Closure<PetriNet>> customUpdates) { + log.debug("Starting updateNetIgnoreRoles for currentNet: {} and reimported: {}", currentNet?.identifier, reimported?.identifier) if (!currentNet) { log.warn("Net $reimported.identifier does not exist") return @@ -185,6 +188,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { def newPermissions = [:] reimported.permissions.each { id, permissions -> + log.trace("Processing permission for role id: {}", id) def newRole = newProcessRoles[id] if (!newRole && (defaultRole.stringId == id || anonymousRole.stringId == id)) { @@ -200,14 +204,17 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { log.warn("Old role does not exist for role $newRole.importId") return } + log.trace("Mapping new role {} to old role {}", newRole.importId, oldRole.stringId) newPermissions[oldRole.stringId] = permissions } } currentNet.permissions = newPermissions as Map<String, Map<String, Boolean>> currentNet.transitions.each { id, t -> + log.trace("Processing transition roles for transition: {}", t.importId) Map<String, Map<String, Boolean>> oldRoles = new HashMap<>() t.roles.each { roleMongoId, permissions -> + log.trace("Processing transition role with mongoId: {}", roleMongoId) def newRole = newProcessRoles[roleMongoId] if (!newRole && (defaultRole.stringId == roleMongoId || anonymousRole.stringId == roleMongoId)) { @@ -223,6 +230,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { log.warn("Old role does not exist for role $newRole.importId") return } + log.trace("Mapping transition role {} to old role {}", newRole.importId, oldRole.stringId) oldRoles[oldRole.stringId] = permissions } } @@ -243,7 +251,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * Helper method used in updateNetIgnoreRoles method, it sorts PetriNet dataSet alphabetically * @param petriNet Instance of Petri Net */ - static void resolveDataOrder(PetriNet petriNet, Locale locale = Locale.getDefault()) { + static void resolveDataOrder(PetriNet petriNet, Locale locale = Locale.ROOT) { Collator collator = Collator.getInstance(locale) List<Field> fields = new LinkedList<>(petriNet.getDataSet().values()) fields = fields.stream().sorted({ f1, f2 -> @@ -262,6 +270,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param permissions New role permissions on transition */ static void updateTransitionRoles(PetriNet net, String transitionId, ProcessRole role, Map<String, Boolean> permissions) { + log.debug("Updating transition roles for transitionId: {} in net: {}", transitionId, net?.identifier) Transition trans = net.transitions.values().find { it.importId == transitionId } if (!trans) { log.warn("Transition with importId $transitionId not found in net $net.identifier") @@ -305,6 +314,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param fileName File name of new version of Petri Net model */ void updateDataSet(String identifier, String fileName, Closure<PetriNet> customUpdate = null) { + log.debug("Starting updateDataSet for identifier: {} with fileName: {}", identifier, fileName) PetriNet existing = petriNetService.getNewestVersionByIdentifier(identifier) InputStream inputStream = new ClassPathResource("petriNets/$fileName" as String).inputStream PetriNet reimported = getImporter().importPetriNet(inputStream) @@ -329,6 +339,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param title Title of the new Process Role */ ProcessRole createRoleInNet(String identifier, String id, String title, Map<EventType, Event> events = [:]) { + log.debug("Creating role in net with identifier: {}, id: {}, title: {}", identifier, id, title) return createRoleInNet(identifier, id, new I18nString(title), events) } @@ -339,6 +350,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param title Title of the new Process Role */ ProcessRole createRoleInNet(String identifier, String id, I18nString title, Map<EventType, Event> events = [:]) { + log.debug("Creating role in net with identifier: {}, id: {}, title: {}", identifier, id, title?.defaultValue) PetriNet net = petriNetService.getNewestVersionByIdentifier(identifier) ProcessRole role = new ProcessRole() @@ -369,6 +381,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { it.value.type == FieldType.USER }.forEach { entry -> + log.trace("Processing user field role references for field: {}", entry.key) UserField field = (reimportedNet.dataSet[entry.key] as UserField) field.roles = field.roles.collect { roleId -> Optional<ProcessRole> roleOpt = Optional.ofNullable(reimportedNet.roles[roleId]) @@ -380,6 +393,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { return null } else { + log.trace("Mapped user field role {} to {}", roleOpt.get().importId, oldRole.stringId) return oldRole.stringId } @@ -400,6 +414,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param title Title of the new Process Role */ ProcessRole createGlobalRole(String id, String title, Map<EventType, Event> events = [:]) { + log.debug("Creating global role with id: {}, title: {}", id, title) return createGlobalRole(id, new I18nString(title), events) } @@ -409,6 +424,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param title Title of the new Process Role */ ProcessRole createGlobalRole(String id, I18nString title, Map<EventType, Event> events = [:]) { + log.debug("Creating global role with id: {}, title: {}", id, title?.defaultValue) ProcessRole role = new ProcessRole() if (!id.startsWith("global_")) { @@ -432,10 +448,12 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @return the updated existing Petri Net */ PetriNet updateRoleEvents(PetriNet existing, PetriNet reimported) { + log.debug("Starting updateRoleEvents for existing net: {} and reimported net: {}", existing?.identifier, reimported?.identifier) List<ProcessRole> newRoles = reimported.roles.values() as List List<ProcessRole> oldRoles = existing.roles.values() as List newRoles.each { newRole -> + log.trace("Processing role events for role: {}", newRole.importId) ProcessRole role = oldRoles.find { it.importId == newRole.importId } if (!role) { log.warn("No existing role found for importId $newRole.importId, skipping event update") @@ -454,6 +472,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ static void setPetriNet(Case useCase, PetriNet net) { + log.debug("Setting PetriNet for case: {} with net: {}", useCase?.stringId, net?.identifier) PetriNet model = net.clone() model.initializeTokens(useCase.getActivePlaces()) model.initializeArcs(useCase.getDataSet()) @@ -473,12 +492,14 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param net Instance of PetriNet */ static Map<String, Map<String, com.netgrif.application.engine.petrinet.domain.Component>> createDataRefComponentsMap(PetriNet net) { + log.debug("Creating dataRef components map for net: {}", net?.identifier) Map<String, Map<String, com.netgrif.application.engine.petrinet.domain.Component>> componentsMap = [:] - net.transitions.each {transition -> + net.transitions.each { transition -> String transId = transition.key - transition.value.dataSet.each {dataField -> + transition.value.dataSet.each { dataField -> String fieldId = dataField.key if (dataField.value.component) { + log.trace("Adding dataRef component for field: {} in transition: {}", fieldId, transId) if (!componentsMap[fieldId]) { componentsMap.put(fieldId, [(transId) : dataField.value.component]) } else { @@ -497,9 +518,11 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * @param net Instance of PetriNet */ static Map<String, com.netgrif.application.engine.petrinet.domain.Component> createComponentsMap(PetriNet net) { + log.debug("Creating components map for net: {}", net?.identifier) Map<String, com.netgrif.application.engine.petrinet.domain.Component> componentsMap = [:] - net.dataSet.each {dataField -> + net.dataSet.each { dataField -> if (dataField.value.component) { + log.trace("Adding component for field: {}", dataField.key) componentsMap.put(dataField.key, dataField.value.component) } } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy index 89f8a0184e5..d3f6bde5a37 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy @@ -20,6 +20,7 @@ import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Criteria import org.springframework.data.mongodb.core.query.Query import org.springframework.stereotype.Component + /** * A helper class for managing task migrations. * This class extends {@link AbstractMigrationHelper} and provides methods for updating, iterating, @@ -129,7 +130,9 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param filter Instance of Predicate, to filter which tasks should be updated */ void updateTasks(Closure update, Predicate filter) { + log.debug("Starting updateTasks with filter: ${filter.toString()}") log.info("Updating tasks with filter ${filter.toString()} and update ${update.toString()}") + log.trace("Converting filter to query and calling iterate") iterate(update, DEFAULT_PROCESS_OPERATIONS, toQuery(filter)) } @@ -140,6 +143,8 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param filter Instance of Predicate, to filter which tasks should be iterated */ void iterateTasks(Closure update, Closure pageProcessed = DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { + log.debug("Starting iterateTasks with filter: ${filter.toString()}, sleepFor: ${sleepFor}ms") + log.trace("Converting filter to query and calling iterate with pageProcessed closure") iterate(update, pageProcessed, toQuery(filter), sleepFor) } @@ -147,11 +152,13 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * Updates all tasks of a given process. * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter * @param processIdentifier identifier of PetriNet, to filter which tasks should be updated - * @param pageSize Optional attribute to set page size. Default page size 100.0 + * @param pageSize Optional attribute to set page size. Default page size 100 */ - void updateTasksCursor(Closure update, String processIdentifier, double pageSize = 100.0) { + void updateTasksCursor(Closure update, String processIdentifier, int pageSize = 100) { + log.debug("Starting updateTasksCursor for processIdentifier: ${processIdentifier}, pageSize: ${pageSize}") String processId = petriNetService.getNewestVersionByIdentifier(processIdentifier).stringId Query query = new Query(Criteria.where("processId").is(processId)) + log.trace("Created query for processId: ${processId}, calling iterate") iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int) } @@ -160,12 +167,14 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter * @param processIdentifier identifier of PetriNet, to filter which tasks should be updated * @param transitionIds List of transition IDs to limit filter to specific transitions of given processIdentifier - * @param pageSize Optional attribute to set page size. Default page size 100.0 + * @param pageSize Optional attribute to set page size. Default page size 100 */ - void updateSpecificTasksCursor(Closure update, String processIdentifier, List<String> transitionIds, double pageSize = 100.0) { + void updateSpecificTasksCursor(Closure update, String processIdentifier, List<String> transitionIds, int pageSize = 100) { + log.debug("Starting updateSpecificTasksCursor for processIdentifier: ${processIdentifier}, transitionIds: ${transitionIds}, pageSize: ${pageSize}") String processId = petriNetService.getNewestVersionByIdentifier(processIdentifier).stringId Query query = new Query(Criteria.where("processId").is(processId)) query.addCriteria(Criteria.where("transitionId").in(transitionIds)) + log.trace("Created query with criteria for processId: ${processId} and transitionIds: ${transitionIds}, calling iterate") iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int) } @@ -174,7 +183,9 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param update Instance of Closure, which should contain code that will be executed for every Task * @param pageSize Optional attribute to set page size. Default page size 100.0 */ - void updateAllTasksCursor(Closure update, double pageSize = 100.0) { + void updateAllTasksCursor(Closure update, int pageSize = 100) { + log.debug("Starting updateAllTasksCursor with pageSize: ${pageSize}") + log.trace("Calling iterate with empty query to process all tasks") iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int) } @@ -185,7 +196,9 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ void reloadTasks(Case useCase, PetriNet net) { + log.debug("Starting reloadTasks for case: ${useCase.stringId}, net identifier: ${net.identifier}") PetriNetMigrationHelper.setPetriNet(useCase, net) + log.trace("Set PetriNet for case, calling taskService.reloadTasks") taskService.reloadTasks(useCase) } @@ -194,7 +207,9 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param task Instance of Task that will be indexed into elasticsearch index */ void elasticTaskIndex(Task task) { + log.debug("Starting elasticTaskIndex for task: ${task.stringId}") try { + log.trace("Transforming and indexing task: ${task.stringId} into elasticsearch") elasticTaskService.indexNow(elasticTaskMappingService.transform(task)) } catch (Exception e) { log.error("Failed to index $task.stringId", e) @@ -209,8 +224,10 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param permissions Map of permissions for the role */ void addRoleToExistingTasks(ProcessRole role, PetriNet net, List<String> transitionIds, Map<String, Boolean> permissions) { + log.debug("Starting addRoleToExistingTasks for role: ${role.getName()}, net: ${net.identifier}, transitionIds: ${transitionIds}") + log.trace("Calling updateTasks to add role with permissions: ${permissions}") updateTasks({ Task task -> - log.info("Add role '${role.getName()}' with roleId=${role.getImportId()} to transitionId=${task.getTransitionId()} in task ${task.stringId}") + log.trace("Add role '${role.getName()}' with roleId=${role.getImportId()} to transitionId=${task.getTransitionId()} in task ${task.stringId}") task.addRole(role.getStringId(), permissions) }, QTask.task.transitionId.in(transitionIds) & QTask.task.processId.eq(net.getStringId())) } @@ -222,7 +239,9 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param relevantTransitionIds List of transition IDs for permissions update */ void updateTasksPermissions(Case useCase, PetriNet net, List<String> relevantTransitionIds) { + log.debug("Starting updateTasksPermissions for case: ${useCase.stringId}, net: ${net.identifier}, relevantTransitionIds: ${relevantTransitionIds}") useCase.tasks.findAll { it.transition in relevantTransitionIds }.each { taskPair -> + log.trace("Processing task permissions for transition: ${taskPair.transition} in case: ${useCase.stringId}") updateTaskPermissions(useCase, taskPair, net) } } @@ -234,9 +253,11 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ void updateTaskPermissions(Case useCase, TaskPair taskPair, PetriNet net) { + log.debug("Starting updateTaskPermissions for case: ${useCase.stringId}, task transition: ${taskPair.transition}") try { Transition newTransition = net.getTransition(taskPair.transition) Task oldTask = taskService.findOne(taskPair.task) + log.trace("Updating task roles and permissions for task: ${oldTask.stringId}") oldTask.setProcessId(net.stringId) oldTask.setRoles(newTransition.roles) oldTask.setNegativeViewRoles(newTransition.negativeViewRoles) From fd1193048a25e8fe1c53b6f1736826a3ced9e16b Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Thu, 28 May 2026 11:39:43 +0200 Subject: [PATCH 12/20] Remove unused `Assert` import from `CaseMigrationHelper` --- .../engine/migration/helpers/CaseMigrationHelper.groovy | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index 3cf83739946..a6e209579b9 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -13,7 +13,6 @@ import com.netgrif.application.engine.workflow.domain.Case import com.netgrif.application.engine.workflow.domain.DataField import com.querydsl.core.types.Predicate import groovy.util.logging.Slf4j -import org.junit.Assert import org.springframework.data.mongodb.core.BulkOperations import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Criteria @@ -21,8 +20,7 @@ import org.springframework.data.mongodb.core.query.Query import org.springframework.stereotype.Component import java.time.LocalDateTime -import java.util.stream.Collectors - +import java.util.stream.Collectors /** * Helper class for managing migrations of Case objects in the application. * Provides methods for updating and iterating over case objects, filtered From eed1e8dca1eedfab464df03be8d60340773c2764 Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Thu, 28 May 2026 11:40:53 +0200 Subject: [PATCH 13/20] Harden `CaseMigrationHelper` against null values in data field conversions --- .../engine/migration/helpers/CaseMigrationHelper.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index a6e209579b9..c2dfa9bef93 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -223,7 +223,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { log.debug("Starting changeDataFieldsValueFromTextToNumber for case: ${useCase.stringId}, fields to change: ${toChange}") toChange.each { dataFieldID -> DataField dataField = useCase.dataSet[dataFieldID] - if (dataField.value && dataField.value != "") { + if (dataField?.value != null && dataField.value != "") { try { def originalValue = dataField.value dataField.value = dataField.value as double From 084a70628bf1f65b37fa0fae27c1552088223ffe Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Thu, 28 May 2026 11:45:32 +0200 Subject: [PATCH 14/20] Fix null handling in `CaseMigrationHelper` and refine `MigrationTest` imports and assertions --- .../engine/migration/MigrationHelper.groovy | 2 +- .../migration/helpers/CaseMigrationHelper.groovy | 11 ++++++++++- .../application/engine/migration/MigrationTest.groovy | 8 ++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy index 15fe90e1df0..686decb82b3 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy @@ -267,7 +267,7 @@ class MigrationHelper { */ def createGlobalRole(String id, String title, Map<EventType, Event> events = [:]) { log.debug("createGlobalRole called with id: {}, title: {}", id, title) - return petriNetMigrationHelper.createGlobalRole(id, title, event) + return petriNetMigrationHelper.createGlobalRole(id, title, events) } /** diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index c2dfa9bef93..6d6f7c9084f 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -284,6 +284,9 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { log.debug("Starting addChoices for case: ${useCase.stringId}, fields: ${toAdd.keySet()}") toAdd.each { dataFieldID, newChoices -> DataField dataField = useCase.dataSet[dataFieldID] + if (!dataField) { + return + } if (dataField.choices == null) { dataField.setChoices(new HashSet<I18nString>()) } @@ -305,6 +308,9 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { toRemove.each { dataFieldID, choicesToRemove -> log.trace("Removing choices ${choicesToRemove} from field ${dataFieldID} in case: ${useCase.stringId}") DataField dataField = useCase.dataSet[dataFieldID] + if (!dataField) { + return + } if (dataField.value != null) { (dataField.value as Set).removeAll(choicesToRemove) } @@ -324,7 +330,10 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { log.debug("Starting changeFileFieldToFileList for case: ${useCase.stringId}, field: ${fieldId}") FileListFieldValue fileListFieldValue = new FileListFieldValue() DataField dataField = useCase.dataSet[fieldId] - def existingValue = dataField?.value + if (!dataField) { + return + } + def existingValue = dataField.value if (existingValue != null) { fileListFieldValue.namesPaths.add(existingValue as FileFieldValue) } diff --git a/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy b/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy index a92c2e80a4a..b5cb8f9ac8c 100644 --- a/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy @@ -51,19 +51,19 @@ class MigrationTest { void beforeEach() { testHelper.truncateDbs() - new FileInputStream("src/test/resources/nae_2432_v1.xml").withCloseable { is -> + this.class.classLoader.getResourceAsStream("nae_2432_v1.xml").withCloseable { is -> ImportPetriNetEventOutcome netV1Outcome = petriNetService.importPetriNet(is, VersionType.MAJOR, superCreator.getLoggedSuper()) assert netV1Outcome.getNet() != null netV1 = netV1Outcome.getNet() } - new FileInputStream("src/test/resources/nae_2432_v2.xml").withCloseable { is -> + this.class.classLoader.getResourceAsStream("nae_2432_v2.xml").withCloseable { is -> ImportPetriNetEventOutcome netV2Outcome = petriNetService.importPetriNet(is, VersionType.MAJOR, superCreator.getLoggedSuper()) assert netV2Outcome.getNet() != null netV2 = netV2Outcome.getNet() } - (1..10).stream().parallel().forEach { + (1..10).forEach { workflowService.createCase(netV1.stringId, "Net V1 " + it, null, superCreator.loggedSuper, Locale.default) } } @@ -92,7 +92,7 @@ class MigrationTest { assert it.dataSet.containsKey("income") assert it.dataSet.containsKey("recreate_info_text") assert it.enabledRoles.size() == 5 - assert it.tasks.size() == 2 && it.tasks[0].transition == "person_info" && it.tasks[1].transition == "recreate_person" + assert it.tasks.size() == 2 && it.tasks.any {it.transition == "person_info"} && it.tasks.any {it.transition == "recreate_person"} } } } From e3d12d9738f7023d861a80bc615d6db5cc973998 Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Thu, 28 May 2026 17:14:01 +0200 Subject: [PATCH 15/20] - Introduce MigrationError, MigrationErrorPolicy, MigrationErrorHandlingMode and MigrationErrorException - Add shared migration error cache with get, pop, clear and has-error helpers - Support configurable error policies for continuing, failing immediately, failing after a limit, or failing after processing - Wire migration error policy handling into case, task and Petri net migration helpers - Add scoped MigrationHelper.withErrorPolicy support for migration scripts - Move migration page size and error policy settings into MigrationProperties - Remove obsolete MigrationConfigurationProperties - Improve helper constructors and page-size resolution to use unified migration properties - Add migration error handling quick usage documentation - Add migration test Petri nets and tests for case migration, Petri net update, and error handling policiesRefactor and enhance migration error handling: - Replaced `MigrationConfigurationProperties` with granular error-handling mechanisms using `MigrationErrorPolicy` and `MigrationErrorHandlingMode --- .../migration_error_handling_quick_usage.md | 92 +++++ .../engine/migration/MigrationHelper.groovy | 140 ++++++- .../MigrationConfigurationProperties.groovy | 55 --- .../helpers/AbstractMigrationHelper.groovy | 267 +++++++++++- .../helpers/CaseMigrationHelper.groovy | 71 ++-- .../helpers/PetriNetMigrationHelper.groovy | 34 +- .../helpers/TaskMigrationHelper.groovy | 75 ++-- .../migration/model/MigrationError.groovy | 162 ++++++++ .../model/MigrationErrorHandlingMode.groovy | 30 ++ .../model/MigrationErrorPolicy.groovy | 180 ++++++++ .../throwable/MigrationErrorException.groovy | 39 ++ .../properties/MigrationProperties.java | 129 ++++++ .../engine/migration/MigrationTest.groovy | 292 +++++++++++-- src/test/resources/petriNets/nae_2432_v1.xml | 257 ++++++++++++ src/test/resources/petriNets/nae_2432_v2.xml | 388 ++++++++++++++++++ 15 files changed, 2012 insertions(+), 199 deletions(-) create mode 100644 docs/migration/migration_error_handling_quick_usage.md delete mode 100644 src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy create mode 100644 src/main/groovy/com/netgrif/application/engine/migration/model/MigrationError.groovy create mode 100644 src/main/groovy/com/netgrif/application/engine/migration/model/MigrationErrorHandlingMode.groovy create mode 100644 src/main/groovy/com/netgrif/application/engine/migration/model/MigrationErrorPolicy.groovy create mode 100644 src/main/groovy/com/netgrif/application/engine/migration/throwable/MigrationErrorException.groovy create mode 100644 src/test/resources/petriNets/nae_2432_v1.xml create mode 100644 src/test/resources/petriNets/nae_2432_v2.xml diff --git a/docs/migration/migration_error_handling_quick_usage.md b/docs/migration/migration_error_handling_quick_usage.md new file mode 100644 index 00000000000..8b5df28b837 --- /dev/null +++ b/docs/migration/migration_error_handling_quick_usage.md @@ -0,0 +1,92 @@ +## Migration Error Handling — Quick Usage +Use `MigrationErrorPolicy` to control what happens when a migration helper encounters an error. +``` groovy +import com.netgrif.application.engine.migration.model.MigrationErrorPolicy +``` + +### Available policies +``` groovy +MigrationErrorPolicy.continueOnError() // log/cache errors and continue +MigrationErrorPolicy.throwImmediately() // stop on first error +MigrationErrorPolicy.throwAfterLimit(10) // stop after 10 errors +MigrationErrorPolicy.throwAfterProcessing() // process all, then fail if errors exist +``` + +#### Basic usage +``` groovy +migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwAfterProcessing()) { + migrationHelper.updateAllCasesCursor({ Case useCase -> + // migration logic + }) +} +``` + +#### Continue migration and inspect errors +``` groovy +migrationHelper.clearErrors() + +migrationHelper.withErrorPolicy(MigrationErrorPolicy.continueOnError()) { + migrationHelper.updateAllCasesCursor({ Case useCase -> + // migration logic + }) +} + +def errors = migrationHelper.popErrors() + +errors.each { error -> + log.warn("${error.entityType} ${error.entityId}: ${error.message}", error.cause) +} +``` + + +#### Stop after a number of errors +``` groovy +migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwAfterLimit(20)) { + migrationHelper.updateAllTasksCursor({ Task task -> + migrationHelper.elasticTaskIndex(task) + }) +} +``` + + +#### Fail after full processing +``` groovy +import com.netgrif.application.engine.migration.throwable.MigrationErrorException + +try { + migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwAfterProcessing()) { + migrationHelper.updateAllCasesCursor({ Case useCase -> + // migration logic + }) + } +} catch (MigrationErrorException e) { + log.error("Migration failed with ${e.errors.size()} errors") + + e.errors.each { error -> + log.error("${error.entityType} ${error.entityId}: ${error.message}", error.cause) + } + + throw e +} +``` + + +#### Error cache helpers +``` groovy +migrationHelper.clearErrors() // clears cached errors +migrationHelper.hasErrors() // true if errors exist +migrationHelper.getErrors() // returns errors without clearing +migrationHelper.popErrors() // returns errors and clears cache +``` + + +#### Recommended production pattern +``` groovy +migrationHelper.clearErrors() + +migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwAfterProcessing()) { + // migration logic +} +``` + +Use throwAfterProcessing() for most production migrations because it collects a full error report and fails only after all possible records are processed. \ No newline at end of file diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy index 686decb82b3..c8d6f684cea 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy @@ -1,10 +1,13 @@ package com.netgrif.application.engine.migration +import com.netgrif.application.engine.configuration.properties.MigrationProperties import com.netgrif.application.engine.importer.service.Importer import com.netgrif.application.engine.migration.helpers.AbstractMigrationHelper import com.netgrif.application.engine.migration.helpers.CaseMigrationHelper import com.netgrif.application.engine.migration.helpers.PetriNetMigrationHelper import com.netgrif.application.engine.migration.helpers.TaskMigrationHelper +import com.netgrif.application.engine.migration.model.MigrationError +import com.netgrif.application.engine.migration.model.MigrationErrorPolicy import com.netgrif.application.engine.petrinet.domain.I18nString import com.netgrif.application.engine.petrinet.domain.PetriNet import com.netgrif.application.engine.petrinet.domain.events.Event @@ -47,13 +50,18 @@ class MigrationHelper { private PetriNetMigrationHelper petriNetMigrationHelper /** - * Returns the Importer service instance used for importing and processing Petri net models. - * This method delegates to the PetriNetMigrationHelper to retrieve the importer. - * @return Importer service instance + * Configuration properties for migration operations. + * Contains settings such as default error policy and other migration-related configuration. */ - private Importer getImporter() { - return petriNetMigrationHelper.getImporter() - } + @Autowired + private MigrationProperties migrationProperties + + /** + * Thread-local storage for the current migration error policy. + * Allows different threads to maintain their own error handling policies during migration operations. + * If not set, defaults to the policy specified in migrationProperties. + */ + private final ThreadLocal<MigrationErrorPolicy> currentErrorPolicy = new ThreadLocal<>() /** * Closure for updating role events between existing and reimported Petri net models. @@ -67,6 +75,47 @@ class MigrationHelper { petriNetMigrationHelper.updateRoleEvents(existing, reimported) } + /** + * Returns the Importer service instance used for importing and processing Petri net models. + * This method delegates to the PetriNetMigrationHelper to retrieve the importer. + * @return Importer service instance + */ + private Importer getImporter() { + return petriNetMigrationHelper.getImporter() + } + + /** + * Retrieves the current error policy for migration operations. + * If no policy is set in the thread-local storage, returns the default policy from migration properties. + * + * @return the current {@link MigrationErrorPolicy} or the default policy if none is set + */ + MigrationErrorPolicy getCurrentErrorPolicy() { + return currentErrorPolicy.get() ?: MigrationErrorPolicy.defaultErrorPolicy(migrationProperties.errorPolicy) + } + + /** + * Executes the provided closure with a specific error policy, then restores the previous policy. + * This method allows temporary override of the error handling policy for a specific migration operation. + * The previous policy is automatically restored after the closure execution, even if an exception occurs. + * + * @param policy the {@link MigrationErrorPolicy} to use during the closure execution + * @param code the closure containing migration code to execute with the specified error policy + */ + void withErrorPolicy(MigrationErrorPolicy policy, Closure code) { + MigrationErrorPolicy previous = currentErrorPolicy.get() + currentErrorPolicy.set(policy) + try { + code.call() + } finally { + if (previous) { + currentErrorPolicy.set(previous) + } else { + currentErrorPolicy.remove() + } + } + } + /** * Updates all cases filtered by filter Predicate. Update closure is called on each filtered case. * @param update Instance of Closure, which should contain code that will be executed for every Case matched by filter @@ -74,7 +123,7 @@ class MigrationHelper { */ void updateCases(Closure update, Predicate filter) { log.debug("updateCases called with filter: {}", filter) - caseMigrationHelper.updateCases(update, filter) + caseMigrationHelper.updateCases(update, filter, getCurrentErrorPolicy()) } /** @@ -83,9 +132,10 @@ class MigrationHelper { * @param sleepFor Optional attribute to set sleep time (in milliseconds) to sleep for after each iterated page. Default 0ms * @param filter Instance of Predicate, to filter which cases should be iterated */ - void iterateCases(Closure update, Closure pageProcessed = AbstractMigrationHelper.DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { + void iterateCases(Closure update, Closure pageProcessed = AbstractMigrationHelper.DEFAULT_PROCESS_OPERATIONS, + long sleepFor = 0, Predicate filter) { log.debug("iterateCases called with filter: {}, sleepFor: {}", filter, sleepFor) - caseMigrationHelper.iterateCases(update, pageProcessed, sleepFor, filter) + caseMigrationHelper.iterateCases(update, pageProcessed, sleepFor, filter, getCurrentErrorPolicy()) } /** @@ -96,7 +146,7 @@ class MigrationHelper { */ void updateCasesCursor(Closure update, String processIdentifier, int pageSize = 100) { log.debug("updateCasesCursor called with processIdentifier: {}, pageSize: {}", processIdentifier, pageSize) - caseMigrationHelper.updateCasesCursor(update, processIdentifier, pageSize) + caseMigrationHelper.updateCasesCursor(update, processIdentifier, pageSize, getCurrentErrorPolicy()) } /** @@ -106,7 +156,7 @@ class MigrationHelper { */ void updateAllCasesCursor(Closure update, int pageSize = 100) { log.debug("updateAllCasesCursor called with pageSize: {}", pageSize) - caseMigrationHelper.updateAllCasesCursor(update, pageSize) + caseMigrationHelper.updateAllCasesCursor(update, pageSize, getCurrentErrorPolicy()) } /** @@ -116,7 +166,7 @@ class MigrationHelper { */ void updateTasks(Closure update, Predicate filter) { log.debug("updateTasks called with filter: {}", filter) - taskMigrationHelper.updateTasks(update, filter) + taskMigrationHelper.updateTasks(update, filter, getCurrentErrorPolicy()) } /** @@ -127,7 +177,7 @@ class MigrationHelper { */ void iterateTasks(Closure update, Closure pageProcessed = AbstractMigrationHelper.DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { log.debug("iterateTasks called with filter: {}, sleepFor: {}", filter, sleepFor) - taskMigrationHelper.iterateTasks(update, pageProcessed, sleepFor, filter) + taskMigrationHelper.iterateTasks(update, pageProcessed, sleepFor, filter, getCurrentErrorPolicy()) } /** @@ -138,7 +188,7 @@ class MigrationHelper { */ void updateTasksCursor(Closure update, String processIdentifier, int pageSize = 100) { log.debug("updateTasksCursor called with processIdentifier: {}, pageSize: {}", processIdentifier, pageSize) - taskMigrationHelper.updateTasksCursor(update, processIdentifier, pageSize) + taskMigrationHelper.updateTasksCursor(update, processIdentifier, pageSize, getCurrentErrorPolicy()) } /** @@ -150,7 +200,7 @@ class MigrationHelper { */ void updateSpecificTasksCursor(Closure update, String processIdentifier, List<String> transitionIds, int pageSize = 100) { log.debug("updateSpecificTasksCursor called with processIdentifier: {}, transitionIds: {}, pageSize: {}", processIdentifier, transitionIds, pageSize) - taskMigrationHelper.updateSpecificTasksCursor(update, processIdentifier, transitionIds, pageSize) + taskMigrationHelper.updateSpecificTasksCursor(update, processIdentifier, transitionIds, pageSize, getCurrentErrorPolicy()) } /** @@ -160,7 +210,7 @@ class MigrationHelper { */ void updateAllTasksCursor(Closure update, int pageSize = 100) { log.debug("updateAllTasksCursor called with pageSize: {}", pageSize) - taskMigrationHelper.updateAllTasksCursor(update, pageSize) + taskMigrationHelper.updateAllTasksCursor(update, pageSize, getCurrentErrorPolicy()) } /** @@ -298,7 +348,7 @@ class MigrationHelper { */ void elasticIndex(Case useCase) { log.debug("elasticIndex called with useCase: {}", useCase?.stringId) - caseMigrationHelper.elasticIndex(useCase) + caseMigrationHelper.elasticIndex(useCase, getCurrentErrorPolicy()) } /** @@ -307,7 +357,7 @@ class MigrationHelper { */ void elasticTaskIndex(Task task) { log.debug("elasticTaskIndex called with task: {}", task?.stringId) - taskMigrationHelper.elasticTaskIndex(task) + taskMigrationHelper.elasticTaskIndex(task, getCurrentErrorPolicy()) } /** @@ -443,7 +493,7 @@ class MigrationHelper { */ void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false) { log.debug("updateCasePermissionsFromNet called with useCase: {}, net: {}, updateTasks: {}", useCase?.stringId, net?.identifier, updateTasks) - caseMigrationHelper.updateCasePermissionsFromNet(useCase, net, updateTasks) + caseMigrationHelper.updateCasePermissionsFromNet(useCase, net, updateTasks, getCurrentErrorPolicy()) } /** @@ -454,7 +504,7 @@ class MigrationHelper { */ void updateTasksPermissions(Case useCase, PetriNet net, List<String> relevantTransitionIds) { log.debug("updateTasksPermissions called with useCase: {}, net: {}, relevantTransitionIds: {}", useCase?.stringId, net?.identifier, relevantTransitionIds) - taskMigrationHelper.updateTasksPermissions(useCase, net, relevantTransitionIds) + taskMigrationHelper.updateTasksPermissions(useCase, net, relevantTransitionIds, getCurrentErrorPolicy()) } /** @@ -464,8 +514,8 @@ class MigrationHelper { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ void updateTaskPermissions(Case useCase, TaskPair taskPair, PetriNet net) { - log.debug("updateTaskPermissions called with useCase: {}, taskPair: {}, net: {}", useCase?.stringId, taskPair?.task?.stringId, net?.identifier) - taskMigrationHelper.updateTaskPermissions(useCase, taskPair, net) + log.debug("updateTaskPermissions called with useCase: {}, taskPair: {}, net: {}", useCase?.stringId, taskPair?.task?.toString(), net?.identifier) + taskMigrationHelper.updateTaskPermissions(useCase, taskPair, net, getCurrentErrorPolicy()) } /** @@ -476,4 +526,50 @@ class MigrationHelper { static void migratePetriNet(Case useCase, PetriNet newNet) { CaseMigrationHelper.migratePetriNet(useCase, newNet) } + + /** + * Returns cached migration errors without clearing them. + * + * @return immutable snapshot of cached migration errors + */ + List<MigrationError> getErrors() { + return AbstractMigrationHelper.getErrors() + } + + /** + * Returns cached migration errors and clears the cache. + * + * @return cached migration errors collected since the last clear/pop + */ + List<MigrationError> popErrors() { + return AbstractMigrationHelper.popErrors() + } + + /** + * Clears cached migration errors. + */ + void clearErrors() { + AbstractMigrationHelper.clearErrors() + } + + /** + * Indicates whether any migration errors were cached. + * + * @return true if at least one error is cached + */ + boolean hasErrors() { + return AbstractMigrationHelper.hasErrors() + } + + /** + * Runs migration code with a clean error cache and returns errors collected during execution. + * + * @param migrationCode migration logic to execute + * @return errors collected during migrationCode execution + */ + List<MigrationError> collectErrors(Closure migrationCode) { + clearErrors() + migrationCode.call() + return popErrors() + } } \ No newline at end of file diff --git a/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy b/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy deleted file mode 100644 index 302300fa357..00000000000 --- a/src/main/groovy/com/netgrif/application/engine/migration/config/properties/MigrationConfigurationProperties.groovy +++ /dev/null @@ -1,55 +0,0 @@ -package com.netgrif.application.engine.migration.config.properties - - -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.stereotype.Component - -@Component -@ConfigurationProperties(prefix = "netgrif.migration") -class MigrationConfigurationProperties { - - private CaseMigrationProperties cases = new CaseMigrationProperties() - - private TaskMigrationProperties tasks = new TaskMigrationProperties() - - private PetriNetMigrationProperties petriNets = new PetriNetMigrationProperties() - - static class CaseMigrationProperties { - - private int pageSize = 100 - - int getPageSize() { - return pageSize - } - } - - static class TaskMigrationProperties { - - private int pageSize = 100 - - int getPageSize() { - return pageSize - } - } - - static class PetriNetMigrationProperties { - - private int pageSize = 100 - - int getPageSize() { - return pageSize - } - } - - CaseMigrationProperties getCases() { - return cases - } - - TaskMigrationProperties getTasks() { - return tasks - } - - PetriNetMigrationProperties getPetriNets() { - return petriNets - } -} diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy index 17e566a2880..eca0b860817 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy @@ -2,8 +2,12 @@ package com.netgrif.application.engine.migration.helpers import com.mongodb.BulkWriteException import com.mongodb.bulk.BulkWriteResult +import com.netgrif.application.engine.configuration.properties.MigrationProperties +import com.netgrif.application.engine.migration.model.MigrationError +import com.netgrif.application.engine.migration.model.MigrationErrorHandlingMode +import com.netgrif.application.engine.migration.model.MigrationErrorPolicy +import com.netgrif.application.engine.migration.throwable.MigrationErrorException import com.netgrif.application.engine.utils.MongodbUtils -import com.netgrif.application.engine.workflow.domain.Case import com.querydsl.core.types.Predicate import groovy.util.logging.Slf4j import org.springframework.data.mongodb.core.BulkOperations @@ -11,13 +15,16 @@ import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Query import org.springframework.data.util.CloseableIterator +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicInteger + /** * AbstractMigrationHelper is an abstract utility class to facilitate the bulk migration of * MongoDB documents. The class provides mechanisms for iterating over documents, preparing * bulk migration operations, and executing those operations efficiently using Spring Data MongoDB's * BulkOperations. It is generic and requires the subtype (document type) to be specified. * - * @param <T> The type of documents this helper will operate on. + * @param <T> The type of documents this helper will operate on. */ @Slf4j abstract class AbstractMigrationHelper<T> { @@ -26,7 +33,7 @@ abstract class AbstractMigrationHelper<T> { * Default Closure used to process bulk operations. It uses the {@link #handleBulkOps} method * to safely execute the bulk operations and log results or errors. */ - static final Closure DEFAULT_PROCESS_OPERATIONS = { BulkOperations bulkOperations -> handleBulkOps(bulkOperations) } + static final Closure DEFAULT_PROCESS_OPERATIONS = { BulkOperations bulkOperations, Class<?> type -> handleBulkOps(bulkOperations, type) } /** @@ -34,23 +41,40 @@ abstract class AbstractMigrationHelper<T> { * It is expected to be provided by subclasses, as the class itself is generic and requires * specific document type initialization to perform the corresponding operations. */ - private final Class<T> type + protected final Class<T> type /** * The {@link MongoTemplate} used for interacting with the MongoDB database. * This is the core dependency of the helper class, allowing it to execute queries, * bulk operations, and other database operations on the specified document type. */ - private final MongoTemplate mongoTemplate + protected final MongoTemplate mongoTemplate + + /** + * Configuration properties for migration operations, providing settings such as error handling policies, + * page sizes, and other migration-related parameters used throughout the migration process. + */ + protected final MigrationProperties migrationProperties + + /** + * A thread-safe map that stores migration errors encountered during the migration process. + * The map is keyed by a string identifier (typically a document ID or migration step identifier) + * and contains a list of {@link MigrationError} objects representing all errors associated with that key. + * This structure allows for efficient error tracking and reporting during bulk migration operations. + */ + private static final List<MigrationError> MIGRATION_ERRORS = new CopyOnWriteArrayList<>() /** * Constructs a new AbstractMigrationHelper with the specified MongoTemplate. * * @param mongoTemplate the {@link MongoTemplate} to use for interacting with MongoDB */ - AbstractMigrationHelper(Class<T> type, MongoTemplate mongoTemplate) { + AbstractMigrationHelper(Class<T> type, + MongoTemplate mongoTemplate, + MigrationProperties migrationProperties) { this.type = type this.mongoTemplate = mongoTemplate + this.migrationProperties = migrationProperties } /** @@ -71,35 +95,103 @@ abstract class AbstractMigrationHelper<T> { */ abstract void prepareOperations(T document, Closure update, BulkOperations bulkOperations) + /** + * Resolves and extracts the unique identifier from the given document. + * This method must be implemented by subclasses to provide the logic for determining + * the document's ID, which is used for error reporting and logging during migration operations. + * The implementation should handle the specific ID field structure of the document type. + * + * @param document the document from which to resolve the identifier + * @return the unique identifier of the document as a String, or null if the ID cannot be resolved + */ + abstract String resolveId(T document) + + /** + * Caches a migration error into the thread-safe error list for later retrieval and reporting. + * This method is typically called when an error occurs during document migration operations, + * allowing the migration process to continue while collecting all errors for review. + * + * @param helper the name or identifier of the migration helper where the error occurred + * @param operation the specific operation being performed when the error occurred + * @param entityType the type of entity (document type) being migrated + * @param entityId the unique identifier of the entity that caused the error + * @param message a descriptive message explaining the error + * @param cause the optional {@link Throwable} that caused the error; defaults to null + */ + static void cacheError(String helper, + String operation, + Class<?> entityType, + String entityId, + String message, + Throwable cause = null) { + MIGRATION_ERRORS.add(MigrationError.of(helper, operation, entityType, entityId, message, cause)) + } + + /** + * Returns an unmodifiable view of all migration errors collected during the migration process. + * The returned list is a snapshot of the current errors and will not reflect any subsequent + * changes to the error cache. + * + * @return an unmodifiable {@link List} of {@link MigrationError} objects + */ + static List<MigrationError> getErrors() { + return Collections.unmodifiableList(new ArrayList<>(MIGRATION_ERRORS)) + } + + /** + * Retrieves all cached migration errors and clears the error cache in a single operation. + * This method is useful for retrieving errors for reporting purposes while simultaneously + * resetting the error cache for a new migration operation. + * + * @return a {@link List} of all {@link MigrationError} objects that were cached + */ + static List<MigrationError> popErrors() { + List<MigrationError> errors = new ArrayList<>(MIGRATION_ERRORS) + MIGRATION_ERRORS.clear() + return errors + } + + /** + * Clears all cached migration errors from the error list. + * This method should be called to reset the error cache before starting a new migration + * operation or after errors have been processed and reported. + */ + static void clearErrors() { + MIGRATION_ERRORS.clear() + } + + /** + * Checks whether any migration errors have been cached. + * This method is useful for quickly determining if any errors occurred during + * the migration process without retrieving the full error list. + * + * @return {@code true} if one or more errors are cached, {@code false} otherwise + */ + static boolean hasErrors() { + return !MIGRATION_ERRORS.isEmpty() + } + /** * A static method to handle the execution of bulk operations. * It executes the given {@link BulkOperations} instance and logs the results or any errors. * * @param bulkOps the bulk operations to execute */ - static void handleBulkOps(BulkOperations bulkOps) { + static void handleBulkOps(BulkOperations bulkOps, Class<?> type) { try { BulkWriteResult bulkWriteResult = bulkOps.execute() log.debug("Processed bulk write of ${bulkWriteResult.modifiedCount}") } catch (BulkWriteException e) { log.error("Failed to write bulk operation", e) e.getWriteErrors().forEach { - log.error("Error writing document with ID ${it.toString()}. Cause: ${it.getMessage()}") + String message = "Error writing document with ID ${it.toString()}. Cause: ${it.getMessage()}" + log.error(message) + cacheError(AbstractMigrationHelper.simpleName, "bulkWrite", type, it.toString(), message, e) } throw e } } - /** - * Converts a QueryDSL {@link Predicate} to a MongoDB {@link Query}. - * - * @param predicate the QueryDSL predicate to convert - * @return a MongoDB Query object representing the predicate - */ - protected Query toQuery(Predicate predicate) { - return MongodbUtils.toQuery(mongoTemplate, type, predicate) - } - /** * Iterates over the documents in the collection, applies updates, and executes bulk operations. * The iteration is paginated based on the provided or default page size, and supports customizable @@ -113,7 +205,8 @@ abstract class AbstractMigrationHelper<T> { * @param pageSize the size of each page (number of documents); defaults to the result of {@link #getPageSize()} */ void iterate(Closure update, Closure processOperations = DEFAULT_PROCESS_OPERATIONS, - Query query = new Query(), long sleepFor = 0, int pageSize = getPageSize()) { + Query query = new Query(), long sleepFor = 0, int pageSize = getPageSize(), + MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { if (pageSize <= 0) { throw new IllegalArgumentException("pageSize must be > 0") } @@ -122,18 +215,40 @@ abstract class AbstractMigrationHelper<T> { long numOfPages = Math.ceil(count / pageSize) as long log.info("Processing ${type.getSimpleName()} documents with filter ${query.toString()}: $numOfPages pages") - long page = 1, currentBatchSize = 0 + long page = 1, currentBatchSize = 0, currentBulkOpsSize = 0 query.cursorBatchSize(pageSize) BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, type) try (CloseableIterator<T> cursor = mongoTemplate.stream(query, type)) { while (cursor.hasNext()) { - prepareOperations(cursor.next(), update, bulkOps) + T document = cursor.next() + + try { + prepareOperations(document, update, bulkOps) + currentBulkOpsSize++ + } catch (Exception e) { + String entityId = resolveId(document) + String message = "Failed to prepare migration operation for ${type.simpleName} ${entityId}" + log.error(message, e) + handleMigrationError(errorPolicy, "iterate", type, entityId, message, e) + } + if (++currentBatchSize == pageSize as long || !cursor.hasNext()) { log.debug("Processed ${type.getSimpleName()} document page {} / {}", page, numOfPages) - processOperations(bulkOps) + + try { + if (currentBulkOpsSize > 0) { + processOperations(bulkOps, type) + } + } catch (Exception e) { + String message = "Failed to process ${type.simpleName} bulk operations on page ${page}" + log.error(message, e) + cacheError(this.class.simpleName, "iterate", type, null, message, e) + } + bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, type) currentBatchSize = 0 + currentBulkOpsSize = 0 page++ if (sleepFor > 0) { log.debug("Pausing migration for ${sleepFor} milliseconds") @@ -141,7 +256,115 @@ abstract class AbstractMigrationHelper<T> { } } } + } catch (Exception e) { + String message = "Failed to iterate ${type.simpleName} documents with filter ${query}" + log.error(message, e) + handleMigrationError(errorPolicy, "iterate", type, null, message, e) + throw e + } finally { + finishMigrationErrorPolicy(errorPolicy) } } } + + /** + * Returns the default migration error policy configured in the application properties. + * This policy determines how errors should be handled during migration operations, + * including whether to cache errors, throw exceptions immediately, or continue processing. + * + * @return a {@link MigrationErrorPolicy} instance based on the configured migration properties + */ + MigrationErrorPolicy defaultErrorPolicy() { + return MigrationErrorPolicy.defaultErrorPolicy(migrationProperties.errorPolicy) + } + + /** + * Converts a QueryDSL {@link Predicate} to a MongoDB {@link Query}. + * This method delegates to the {@link MongodbUtils} utility to perform the conversion, + * using the current MongoTemplate and document type. + * + * @param predicate the QueryDSL predicate to convert + * @return a MongoDB Query object representing the predicate + */ + protected Query toQuery(Predicate predicate) { + return MongodbUtils.toQuery(mongoTemplate, type, predicate) + } + + /** + * Handles migration errors according to the specified error policy. + * This method implements different error handling strategies based on the policy mode, + * including caching errors, throwing exceptions immediately, throwing after reaching an error limit, + * or continuing processing to throw after all operations complete. + * + * @param policy the {@link MigrationErrorPolicy} defining how to handle the error + * @param operation the name of the operation being performed when the error occurred + * @param type the class type of the entity being migrated + * @param entityId the unique identifier of the entity that caused the error, or null if not applicable + * @param message a descriptive message explaining the error + * @param cause the optional {@link Throwable} that caused the error; defaults to null + * @throws MigrationErrorException if the error policy requires throwing an exception + */ + protected void handleMigrationError(MigrationErrorPolicy policy, String operation, Class<?> type, String entityId, + String message, Throwable cause = null) { + if (policy.cacheErrors) { + cacheError(this.class.simpleName, operation, type, entityId, message, cause) + } + + switch (policy.mode) { + case MigrationErrorHandlingMode.THROW_IMMEDIATELY: + throwError(policy, message, cause) + break + case MigrationErrorHandlingMode.THROW_AFTER_LIMIT: + if (getErrors().size() >= policy.maxErrors) { + throw new MigrationErrorException("Migration failed after reaching error limit ${policy.maxErrors}", getErrors(), cause) + } + break + case MigrationErrorHandlingMode.CONTINUE: + break + case MigrationErrorHandlingMode.THROW_AFTER_PROCESSING: + break + } + } + + /** + * Throws an exception based on the specified error policy and error details. + * If the policy specifies throwing the original exception and the cause is a RuntimeException, + * the original exception is re-thrown. Otherwise, a new {@link MigrationErrorException} is thrown + * containing the message, all cached errors, and the original cause. + * + * @param policy the {@link MigrationErrorPolicy} defining how to throw the error + * @param message a descriptive message explaining the error + * @param cause the optional {@link Throwable} that caused the error; defaults to null + * @throws RuntimeException or {@link MigrationErrorException} depending on the policy and cause + */ + protected static void throwError(MigrationErrorPolicy policy, String message, Throwable cause = null) { + if (policy.throwOriginal && cause instanceof RuntimeException) { + throw cause + } + + throw new MigrationErrorException( + message, + getErrors(), + cause + ) + } + + /** + * Finalizes the migration error policy after processing is complete. + * If the error policy mode is {@link MigrationErrorHandlingMode#THROW_AFTER_PROCESSING} + * and errors were collected during processing, throws a {@link MigrationErrorException} + * containing all cached errors. This method should be called in the finally block + * of migration operations to ensure proper error handling. + * + * @param policy the {@link MigrationErrorPolicy} defining the error handling behavior + * @throws MigrationErrorException if the policy requires throwing after processing and errors exist + */ + protected static void finishMigrationErrorPolicy(MigrationErrorPolicy policy) { + if (policy.mode == MigrationErrorHandlingMode.THROW_AFTER_PROCESSING && hasErrors()) { + throw new MigrationErrorException( + "Migration finished with ${getErrors().size()} errors", + getErrors() + ) + } + } } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index 6d6f7c9084f..33733ea3c4f 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -1,9 +1,9 @@ package com.netgrif.application.engine.migration.helpers +import com.netgrif.application.engine.configuration.properties.MigrationProperties import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseMappingService import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService -import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties -import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.CaseMigrationProperties +import com.netgrif.application.engine.migration.model.MigrationErrorPolicy import com.netgrif.application.engine.petrinet.domain.I18nString import com.netgrif.application.engine.petrinet.domain.PetriNet import com.netgrif.application.engine.petrinet.domain.dataset.FileFieldValue @@ -21,6 +21,7 @@ import org.springframework.stereotype.Component import java.time.LocalDateTime import java.util.stream.Collectors + /** * Helper class for managing migrations of Case objects in the application. * Provides methods for updating and iterating over case objects, filtered @@ -33,11 +34,6 @@ import java.util.stream.Collectors @Component class CaseMigrationHelper extends AbstractMigrationHelper<Case> { - /** - * Configuration properties for case migration. - */ - protected final CaseMigrationProperties caseMigrationProperties - /** * Service for managing PetriNet operations. */ @@ -66,13 +62,12 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param migrationConfigurationProperties Properties for migration configuration, including cases. */ CaseMigrationHelper(MongoTemplate mongoTemplate, - MigrationConfigurationProperties migrationConfigurationProperties, + MigrationProperties migrationProperties, IPetriNetService petriNetService, IElasticCaseService elasticCaseService, IElasticCaseMappingService elasticCaseMappingService, TaskMigrationHelper taskMigrationHelper) { - super(Case.class, mongoTemplate) - this.caseMigrationProperties = migrationConfigurationProperties.cases + super(Case.class, mongoTemplate, migrationProperties) this.petriNetService = petriNetService this.elasticCaseService = elasticCaseService this.elasticCaseMappingService = elasticCaseMappingService @@ -86,7 +81,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { */ @Override int getPageSize() { - return caseMigrationProperties.pageSize + return migrationProperties.cases.pageSize } /** @@ -104,6 +99,17 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { bulkOperations.replaceOne(Query.query(Criteria.where("_id").is(useCase.get_id())), useCase) } + /** + * Resolves and retrieves the string representation of the ID for the given Case document. + * + * @param document The Case document whose ID should be resolved. + * @return The string representation of the case's ID. + */ + @Override + String resolveId(Case document) { + return document.getStringId() + } + /** * Updates all cases that match the given filter predicate. The update closure * is executed for each matched case. @@ -111,9 +117,9 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param update A closure containing the code to execute for each matching case. * @param filter A QueryDSL Predicate object specifying the conditions to filter the cases. */ - void updateCases(Closure update, Predicate filter) { + void updateCases(Closure update, Predicate filter, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Updating cases with filter ${filter.toString()} and update ${update.toString()}") - iterate(update, DEFAULT_PROCESS_OPERATIONS, toQuery(filter)) + iterate(update, DEFAULT_PROCESS_OPERATIONS, toQuery(filter), 0, getPageSize(), errorPolicy) } /** @@ -125,9 +131,10 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param sleepFor Optional sleep time (in milliseconds) between processing pages. Default is 0ms. * @param filter A QueryDSL Predicate object specifying the conditions to filter the cases. */ - void iterateCases(Closure update, Closure pageProcessed = DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { + void iterateCases(Closure update, Closure pageProcessed = DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, + Predicate filter, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting iterateCases with filter: ${filter.toString()}, sleepFor: ${sleepFor}ms") - iterate(update, pageProcessed, toQuery(filter), sleepFor) + iterate(update, pageProcessed, toQuery(filter), sleepFor, getPageSize(), errorPolicy) } /** @@ -137,10 +144,11 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param processIdentifier The identifier of the PetriNet process. * @param pageSize Optional page size for processing cases. Default is 100. */ - void updateCasesCursor(Closure update, String processIdentifier, int pageSize = 100) { + void updateCasesCursor(Closure update, String processIdentifier, int pageSize = 100, + MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateCasesCursor for processIdentifier: ${processIdentifier}, pageSize: ${pageSize}") Query query = new Query(Criteria.where("processIdentifier").is(processIdentifier)) - iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int) + iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int, errorPolicy) } /** @@ -149,9 +157,9 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param update A closure containing the code to execute for each case. * @param pageSize Optional page size for processing cases. Default is 100. */ - void updateAllCasesCursor(Closure update, int pageSize = 100) { + void updateAllCasesCursor(Closure update, int pageSize = 100, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateAllCasesCursor with pageSize: ${pageSize}") - iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int) + iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int, errorPolicy) } /** @@ -159,12 +167,14 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * handles useCase.petriNet internally * @param useCase Instance of Case that will be indexed into elasticsearch index */ - void elasticIndex(Case useCase) { + void elasticIndex(Case useCase, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting elasticIndex for case: ${useCase.stringId}") try { PetriNetMigrationHelper.setPetriNet(useCase, petriNetService.get(useCase.petriNetObjectId)) if (!useCase.petriNet) { - log.error("Failed to set petriNet for case $useCase.stringId") + String message = "Failed to set petriNet for case $useCase.stringId" + log.error(message) + cacheError(this.class.simpleName, "elasticIndex", type, useCase.stringId, message) return } log.trace("Successfully set petriNet for case: ${useCase.stringId}") @@ -176,10 +186,14 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { try { elasticCaseService.indexNow(elasticCaseMappingService.transform(useCase)) } catch (Exception retryEx) { - log.error("Failed to index $useCase.stringId after setting lastModified", retryEx) + String message = "Failed to index $useCase.stringId after setting lastModified" + log.error(message, retryEx) + handleMigrationError(errorPolicy, "elasticIndex", type, useCase.stringId, message, ex) } } else { - log.error("Failed to index $useCase.stringId", ex) + String message = "Failed to index $useCase.stringId" + log.error(message, ex) + handleMigrationError(errorPolicy, "elasticIndex", type, useCase.stringId, message, ex) } } } @@ -219,7 +233,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - static void changeDataFieldsValueFromTextToNumber(Case useCase, Set<String> toChange) { + static void changeDataFieldsValueFromTextToNumber(Case useCase, Set<String> toChange, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting changeDataFieldsValueFromTextToNumber for case: ${useCase.stringId}, fields to change: ${toChange}") toChange.each { dataFieldID -> DataField dataField = useCase.dataSet[dataFieldID] @@ -231,7 +245,9 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { } catch (Exception e) { def originalValue = dataField.value dataField.value = null - log.error("[${useCase.stringId}] could not convert value ${originalValue} in field ${dataFieldID}", e) + String message = "[${useCase.stringId}] could not convert value ${originalValue} in field ${dataFieldID}" + log.error(message, e) + handleMigrationError(errorPolicy, "changeDataFieldsValueFromTextToNumber", type, useCase.stringId, message, e) } } } @@ -368,7 +384,8 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ - void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false) { + void updateCasePermissionsFromNet(Case useCase, PetriNet net, boolean updateTasks = false + , MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateCasePermissionsFromNet for case: ${useCase.stringId}, net: ${net.stringId}, updateTasks: ${updateTasks}") useCase.permissions = net.getPermissions().entrySet().stream() .filter(role -> role.getValue().containsKey("delete") || role.getValue().containsKey("view")) @@ -387,7 +404,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { log.trace("Updated permissions and enabled roles for case: ${useCase.stringId}") if (updateTasks) { useCase.tasks.each { taskPair -> - taskMigrationHelper.updateTaskPermissions(useCase, taskPair, net) + taskMigrationHelper.updateTaskPermissions(useCase, taskPair, net, errorPolicy) } } } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy index 4e44b183f61..85d20a75c21 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy @@ -1,9 +1,8 @@ package com.netgrif.application.engine.migration.helpers import com.netgrif.application.engine.auth.service.UserService +import com.netgrif.application.engine.configuration.properties.MigrationProperties import com.netgrif.application.engine.importer.service.Importer -import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties -import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.PetriNetMigrationProperties import com.netgrif.application.engine.petrinet.domain.I18nString import com.netgrif.application.engine.petrinet.domain.PetriNet import com.netgrif.application.engine.petrinet.domain.Transition @@ -52,12 +51,6 @@ import java.util.stream.Collectors @Component class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { - /** - * Configuration properties specific to Petri Net migration operations. - * Contains settings such as page size and other Petri Net-related migration configurations. - */ - protected final PetriNetMigrationProperties petriNetMigrationProperties - /** * Service interface for managing Petri Net operations including importing, saving, and retrieving Petri Net models. */ @@ -83,20 +76,19 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { * Constructs a new PetriNetMigrationHelper with the specified dependencies. * * @param mongoTemplate the {@link MongoTemplate} to use for interacting with MongoDB - * @param migrationConfigurationProperties the {@link MigrationConfigurationProperties} containing migration settings including page size and other configuration + * @param migrationProperties the {@link MigrationProperties} containing migration settings including page size and other configuration * @param petriNetService the {@link IPetriNetService} for managing Petri Net operations such as importing, saving, and retrieving Petri Nets * @param processRoleRepository the {@link ProcessRoleRepository} for persisting and retrieving process roles from the database * @param importerProvider the {@link Provider} that supplies {@link Importer} instances for importing Petri Net models from various sources * @param userService the {@link UserService} for managing user-related operations, including retrieving system user for Petri Net imports */ PetriNetMigrationHelper(MongoTemplate mongoTemplate, - MigrationConfigurationProperties migrationConfigurationProperties, + MigrationProperties migrationProperties, IPetriNetService petriNetService, ProcessRoleRepository processRoleRepository, Provider<Importer> importerProvider, UserService userService) { - super(PetriNet.class, mongoTemplate) - this.petriNetMigrationProperties = migrationConfigurationProperties.petriNets + super(PetriNet.class, mongoTemplate, migrationProperties) this.petriNetService = petriNetService this.processRoleRepository = processRoleRepository this.importerProvider = importerProvider @@ -106,11 +98,11 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { /** * Returns the page size for pagination during migration operations. * - * @return the page size configured in {@link PetriNetMigrationProperties} + * @return the page size configured in {@link MigrationProperties.PetriNetMigrationProperties} */ @Override int getPageSize() { - return petriNetMigrationProperties.pageSize + return migrationProperties.petriNets.pageSize } /** @@ -127,6 +119,20 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper<PetriNet> { bulkOperations.replaceOne(Query.query(Criteria.where("_id").is(document.getObjectId())), document) } + /** + * Resolves and returns the string identifier of a Petri Net document. + * <p> + * This method is used during migration operations to uniquely identify Petri Net documents + * when performing bulk operations or logging migration progress. + * + * @param document the {@link PetriNet} document whose identifier should be resolved + * @return the string representation of the Petri Net's unique identifier + */ + @Override + String resolveId(PetriNet document) { + return document.getStringId() + } + /** * Updates existing Petri Net model with new values. New process roles are ignored! New roles in existing user type fields will be ignored! * @param identifier Identifier of Petri Net model that is being updated diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy index d3f6bde5a37..32f2c8e5c6a 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy @@ -1,9 +1,9 @@ package com.netgrif.application.engine.migration.helpers +import com.netgrif.application.engine.configuration.properties.MigrationProperties import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskMappingService import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService -import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties -import com.netgrif.application.engine.migration.config.properties.MigrationConfigurationProperties.TaskMigrationProperties +import com.netgrif.application.engine.migration.model.MigrationErrorPolicy import com.netgrif.application.engine.petrinet.domain.PetriNet import com.netgrif.application.engine.petrinet.domain.Transition import com.netgrif.application.engine.petrinet.domain.roles.ProcessRole @@ -32,15 +32,6 @@ import org.springframework.stereotype.Component @Component class TaskMigrationHelper extends AbstractMigrationHelper<Task> { - /** - * The task migration properties configuration. - * - * This property provides the configuration values for task migration, - * such as the size of the page used to process tasks in the migration. - * It is loaded from the {@link MigrationConfigurationProperties} during initialization. - */ - protected final TaskMigrationProperties taskMigrationProperties - /** * Service for handling Petri Net operations. * @@ -80,13 +71,12 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param mongoTemplate the {@link MongoTemplate} to use for interacting with MongoDB */ TaskMigrationHelper(MongoTemplate mongoTemplate, - MigrationConfigurationProperties migrationConfigurationProperties, + MigrationProperties migrationProperties, IPetriNetService petriNetService, ITaskService taskService, IElasticTaskService elasticTaskService, IElasticTaskMappingService elasticTaskMappingService) { - super(Task.class, mongoTemplate) - this.taskMigrationProperties = migrationConfigurationProperties.tasks + super(Task.class, mongoTemplate, migrationProperties) this.petriNetService = petriNetService this.taskService = taskService this.elasticTaskService = elasticTaskService @@ -96,14 +86,14 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { /** * Returns the page size for the task migration process. * - * The page size is configured in the {@link TaskMigrationProperties} and determines + * The page size is configured in the {@link MigrationProperties.TaskMigrationProperties} and determines * the number of tasks processed in a single batch during migration operations. * * @return an integer indicating the configured page size */ @Override int getPageSize() { - return taskMigrationProperties.pageSize + return migrationProperties.tasks.pageSize } /** @@ -124,16 +114,30 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { bulkOperations.replaceOne(Query.query(Criteria.where("_id").is(document.getObjectId())), document) } + /** + * Resolves and returns the unique identifier of the given task document. + * + * This method extracts the string representation of the task's identifier, + * which is used for logging and tracking purposes during migration operations. + * + * @param document the {@link Task} document whose identifier should be resolved + * @return a {@link String} representing the unique identifier of the task + */ + @Override + String resolveId(Task document) { + return document.getStringId() + } + /** * Updates all tasks filtered by filter Predicate. Update closure is called on each filtered task. * @param update Instance of Closure, which should contain code that will be executed for every Task matched by filter * @param filter Instance of Predicate, to filter which tasks should be updated */ - void updateTasks(Closure update, Predicate filter) { + void updateTasks(Closure update, Predicate filter, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateTasks with filter: ${filter.toString()}") log.info("Updating tasks with filter ${filter.toString()} and update ${update.toString()}") log.trace("Converting filter to query and calling iterate") - iterate(update, DEFAULT_PROCESS_OPERATIONS, toQuery(filter)) + iterate(update, DEFAULT_PROCESS_OPERATIONS, toQuery(filter), 0, getPageSize(), errorPolicy) } /** @@ -142,10 +146,11 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param sleepFor Optional attribute to set sleep time (in milliseconds) to sleep for after each iterated page. Default 0ms * @param filter Instance of Predicate, to filter which tasks should be iterated */ - void iterateTasks(Closure update, Closure pageProcessed = DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { + void iterateTasks(Closure update, Closure pageProcessed = DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter, + MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting iterateTasks with filter: ${filter.toString()}, sleepFor: ${sleepFor}ms") log.trace("Converting filter to query and calling iterate with pageProcessed closure") - iterate(update, pageProcessed, toQuery(filter), sleepFor) + iterate(update, pageProcessed, toQuery(filter), sleepFor, getPageSize(), errorPolicy) } /** @@ -154,12 +159,13 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param processIdentifier identifier of PetriNet, to filter which tasks should be updated * @param pageSize Optional attribute to set page size. Default page size 100 */ - void updateTasksCursor(Closure update, String processIdentifier, int pageSize = 100) { + void updateTasksCursor(Closure update, String processIdentifier, int pageSize = 100, + MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateTasksCursor for processIdentifier: ${processIdentifier}, pageSize: ${pageSize}") String processId = petriNetService.getNewestVersionByIdentifier(processIdentifier).stringId Query query = new Query(Criteria.where("processId").is(processId)) log.trace("Created query for processId: ${processId}, calling iterate") - iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int) + iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int, errorPolicy) } /** @@ -169,13 +175,14 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param transitionIds List of transition IDs to limit filter to specific transitions of given processIdentifier * @param pageSize Optional attribute to set page size. Default page size 100 */ - void updateSpecificTasksCursor(Closure update, String processIdentifier, List<String> transitionIds, int pageSize = 100) { + void updateSpecificTasksCursor(Closure update, String processIdentifier, List<String> transitionIds, int pageSize = 100, + MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateSpecificTasksCursor for processIdentifier: ${processIdentifier}, transitionIds: ${transitionIds}, pageSize: ${pageSize}") String processId = petriNetService.getNewestVersionByIdentifier(processIdentifier).stringId Query query = new Query(Criteria.where("processId").is(processId)) query.addCriteria(Criteria.where("transitionId").in(transitionIds)) log.trace("Created query with criteria for processId: ${processId} and transitionIds: ${transitionIds}, calling iterate") - iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int) + iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int, errorPolicy) } /** @@ -183,10 +190,10 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param update Instance of Closure, which should contain code that will be executed for every Task * @param pageSize Optional attribute to set page size. Default page size 100.0 */ - void updateAllTasksCursor(Closure update, int pageSize = 100) { + void updateAllTasksCursor(Closure update, int pageSize = 100, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateAllTasksCursor with pageSize: ${pageSize}") log.trace("Calling iterate with empty query to process all tasks") - iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int) + iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int, errorPolicy) } /** @@ -206,13 +213,15 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * Indexes provided task in elasticsearch * @param task Instance of Task that will be indexed into elasticsearch index */ - void elasticTaskIndex(Task task) { + void elasticTaskIndex(Task task, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting elasticTaskIndex for task: ${task.stringId}") try { log.trace("Transforming and indexing task: ${task.stringId} into elasticsearch") elasticTaskService.indexNow(elasticTaskMappingService.transform(task)) } catch (Exception e) { - log.error("Failed to index $task.stringId", e) + String message = "Failed to index $task.stringId" + log.error(message, e) + handleMigrationError(errorPolicy, "elasticTaskIndex", type, task.stringId, message, e) } } @@ -238,11 +247,11 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param net Instance of Petri Net, it needs to match processIdentifier of useCase * @param relevantTransitionIds List of transition IDs for permissions update */ - void updateTasksPermissions(Case useCase, PetriNet net, List<String> relevantTransitionIds) { + void updateTasksPermissions(Case useCase, PetriNet net, List<String> relevantTransitionIds, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateTasksPermissions for case: ${useCase.stringId}, net: ${net.identifier}, relevantTransitionIds: ${relevantTransitionIds}") useCase.tasks.findAll { it.transition in relevantTransitionIds }.each { taskPair -> log.trace("Processing task permissions for transition: ${taskPair.transition} in case: ${useCase.stringId}") - updateTaskPermissions(useCase, taskPair, net) + updateTaskPermissions(useCase, taskPair, net, errorPolicy) } } @@ -252,7 +261,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param taskPair TaskPair object of updated Task * @param net Instance of Petri Net, it needs to match processIdentifier of useCase */ - void updateTaskPermissions(Case useCase, TaskPair taskPair, PetriNet net) { + void updateTaskPermissions(Case useCase, TaskPair taskPair, PetriNet net, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateTaskPermissions for case: ${useCase.stringId}, task transition: ${taskPair.transition}") try { Transition newTransition = net.getTransition(taskPair.transition) @@ -264,7 +273,9 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { oldTask.resolveViewRoles() taskService.save(oldTask) } catch (Exception e) { - log.error("Failed to update task permissions $useCase.stringId $taskPair.transition", e) + String message = "Failed to update task permissions $useCase.stringId $taskPair.transition" + log.error(message, e) + handleMigrationError(errorPolicy, "updateTaskPermissions", type, taskPair?.task?.toString(), message, e) } } } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationError.groovy b/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationError.groovy new file mode 100644 index 00000000000..493802ae379 --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationError.groovy @@ -0,0 +1,162 @@ +package com.netgrif.application.engine.migration.model + +import java.time.LocalDateTime + +/** + * Represents an error that occurred during a migration operation. + * <p> + * This class captures detailed information about migration failures including timestamp, + * the helper class involved, the operation being performed, the entity type and ID, + * error message, and the underlying cause of the error. + * </p> + */ +class MigrationError { + + + /** + * The timestamp when the error occurred. + */ + private LocalDateTime timestamp + + /** + * The name of the helper class where the error occurred. + */ + private String helper + + /** + * The operation being performed when the error occurred. + */ + private String operation + + /** + * The type of entity involved in the migration. + */ + private Class<?> entityType + + /** + * The ID of the entity involved in the migration. + */ + private String entityId + + /** + * A descriptive error message. + */ + private String message + + /** + * The underlying exception that caused the error, or null if none. + */ + private Throwable cause + + /** + * Constructs a new MigrationError with the specified details. + * + * @param timestamp the timestamp when the error occurred + * @param helper the name of the helper class where the error occurred + * @param operation the operation being performed when the error occurred + * @param entityType the type of entity involved in the migration + * @param entityId the ID of the entity involved in the migration + * @param message a descriptive error message + * @param cause the underlying exception that caused the error, or null if none + */ + MigrationError(LocalDateTime timestamp, String helper, String operation, Class<?> entityType, String entityId, String message, Throwable cause) { + this.timestamp = timestamp + this.helper = helper + this.operation = operation + this.entityType = entityType + this.entityId = entityId + this.message = message + this.cause = cause + } + + /** + * Factory method to create a new MigrationError with the current timestamp. + * + * @param helper the name of the helper class where the error occurred + * @param operation the operation being performed when the error occurred + * @param entityType the type of entity involved in the migration + * @param entityId the ID of the entity involved in the migration + * @param message a descriptive error message + * @param cause the underlying exception that caused the error (optional, defaults to null) + * @return a new MigrationError instance with the current timestamp + */ + static MigrationError of(String helper, + String operation, + Class<?> entityType, + String entityId, + String message, + Throwable cause = null) { + return new MigrationError( + LocalDateTime.now(), + helper, + operation, + entityType, + entityId, + message, + cause + ) + } + + /** + * Returns the timestamp when the error occurred. + * + * @return the timestamp of the error + */ + LocalDateTime getTimestamp() { + return timestamp + } + + /** + * Returns the name of the helper class where the error occurred. + * + * @return the helper class name + */ + String getHelper() { + return helper + } + + /** + * Returns the operation being performed when the error occurred. + * + * @return the operation name + */ + String getOperation() { + return operation + } + + /** + * Returns the type of entity involved in the migration. + * + * @return the entity type + */ + String getEntityType() { + return entityType + } + + /** + * Returns the ID of the entity involved in the migration. + * + * @return the entity ID + */ + String getEntityId() { + return entityId + } + + /** + * Returns the descriptive error message. + * + * @return the error message + */ + String getMessage() { + return message + } + + /** + * Returns the underlying exception that caused the error. + * + * @return the cause of the error, or null if there is no underlying cause + */ + Throwable getCause() { + return cause + } +} diff --git a/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationErrorHandlingMode.groovy b/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationErrorHandlingMode.groovy new file mode 100644 index 00000000000..25f2b228348 --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationErrorHandlingMode.groovy @@ -0,0 +1,30 @@ +package com.netgrif.application.engine.migration.model + +/** + * Defines the error handling strategies for migration operations. + * <p> + * This enum specifies how errors encountered during migration should be handled, + * allowing control over whether to fail fast, continue processing, or apply limits. + */ +enum MigrationErrorHandlingMode { + + /** + * Cache/log error and immediately throw. + */ + THROW_IMMEDIATELY, + + /** + * Cache/log error and continue migration. + */ + CONTINUE, + + /** + * Cache/log error and throw once maxErrors is reached. + */ + THROW_AFTER_LIMIT, + + /** + * Cache/log error and continue processing, but throw after the operation finishes if any errors occurred. + */ + THROW_AFTER_PROCESSING +} \ No newline at end of file diff --git a/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationErrorPolicy.groovy b/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationErrorPolicy.groovy new file mode 100644 index 00000000000..6334bf7b7c9 --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationErrorPolicy.groovy @@ -0,0 +1,180 @@ +package com.netgrif.application.engine.migration.model + +import com.netgrif.application.engine.configuration.properties.MigrationProperties + +/** + * Configuration class that defines how errors should be handled during migration processes. + * <p> + * This policy allows fine-grained control over error handling behavior including: + * <ul> + * <li>When to throw exceptions (immediately, after a limit, after processing, or continue)</li> + * <li>Whether to cache encountered errors</li> + * <li>Maximum number of errors to tolerate before throwing</li> + * <li>Whether to rethrow original exceptions or wrap them</li> + * </ul> + * <p> + * Factory methods are provided for common error handling scenarios. + * + * @see MigrationErrorHandlingMode + */ + +class MigrationErrorPolicy { + + /** + * The error handling mode that determines when exceptions should be thrown. + * Defaults to {@link MigrationErrorHandlingMode#CONTINUE}. + */ + private MigrationErrorHandlingMode mode = MigrationErrorHandlingMode.CONTINUE + + /** + * Maximum number of cached errors before throwing. + * Used when mode is THROW_AFTER_LIMIT. + */ + private int maxErrors = 0 + + /** + * Whether encountered errors should be stored in the migration error cache. + */ + private boolean cacheErrors = true + + /** + * Whether to rethrow the original exception where possible. + * If false, throw MigrationErrorException with cached errors. + * Defaults to false. + */ + private boolean throwOriginal = false + + /** + * Creates a default error policy based on application configuration properties. + * This factory method reads error handling settings from the provided migration properties + * and constructs a MigrationErrorPolicy with those settings. + * + * @param migrationProperties the migration configuration properties containing error policy settings + * @return a new MigrationErrorPolicy configured according to the application properties + */ + static MigrationErrorPolicy defaultErrorPolicy(MigrationProperties.ErrorPolicy props) { + return new MigrationErrorPolicy( + mode: MigrationErrorHandlingMode.valueOf(props.mode), + maxErrors: props.maxErrors, + cacheErrors: props.cacheErrors, + throwOriginal: props.throwOriginal + ) + } + + /** + * Creates a policy that continues processing even when errors occur. + * Errors will be cached but will not stop the migration process. + * + * @return a new MigrationErrorPolicy configured to continue on error + */ + static MigrationErrorPolicy continueOnError() { + return new MigrationErrorPolicy(mode: MigrationErrorHandlingMode.CONTINUE) + } + + /** + * Creates a policy that throws an exception immediately when the first error is encountered. + * This stops the migration process as soon as any error occurs. + * + * @return a new MigrationErrorPolicy configured to throw immediately on error + */ + static MigrationErrorPolicy throwImmediately() { + return new MigrationErrorPolicy(mode: MigrationErrorHandlingMode.THROW_IMMEDIATELY) + } + + /** + * Creates a policy that throws an exception after a specified number of errors have been encountered. + * This allows the migration to tolerate a limited number of errors before failing. + * + * @param maxErrors the maximum number of errors to cache before throwing an exception + * @return a new MigrationErrorPolicy configured to throw after reaching the error limit + */ + static MigrationErrorPolicy throwAfterLimit(int maxErrors) { + return new MigrationErrorPolicy( + mode: MigrationErrorHandlingMode.THROW_AFTER_LIMIT, + maxErrors: maxErrors + ) + } + + /** + * Creates a policy that completes the migration process and throws an exception afterward if any errors occurred. + * This allows all migration steps to be attempted before reporting failures. + * + * @return a new MigrationErrorPolicy configured to throw after processing completes + */ + static MigrationErrorPolicy throwAfterProcessing() { + return new MigrationErrorPolicy(mode: MigrationErrorHandlingMode.THROW_AFTER_PROCESSING) + } + + /** + * Gets the current error handling mode. + * + * @return the configured error handling mode + */ + MigrationErrorHandlingMode getMode() { + return mode + } + + /** + * Sets the error handling mode. + * + * @param mode the error handling mode to use + */ + void setMode(MigrationErrorHandlingMode mode) { + this.mode = mode + } + + /** + * Gets the maximum number of errors allowed before throwing an exception. + * Only relevant when mode is {@link MigrationErrorHandlingMode#THROW_AFTER_LIMIT}. + * + * @return the maximum error count threshold + */ + int getMaxErrors() { + return maxErrors + } + + /** + * Sets the maximum number of errors allowed before throwing an exception. + * + * @param maxErrors the maximum error count threshold + */ + void setMaxErrors(int maxErrors) { + this.maxErrors = maxErrors + } + + /** + * Checks whether errors should be cached during migration. + * + * @return true if errors should be cached, false otherwise + */ + boolean getCacheErrors() { + return cacheErrors + } + + /** + * Sets whether errors should be cached during migration. + * + * @param cacheErrors true to cache errors, false otherwise + */ + void setCacheErrors(boolean cacheErrors) { + this.cacheErrors = cacheErrors + } + + /** + * Checks whether original exceptions should be rethrown. + * + * @return true if original exceptions should be rethrown, false to wrap them in MigrationErrorException + */ + boolean getThrowOriginal() { + return throwOriginal + } + + /** + * Sets whether original exceptions should be rethrown. + * + * @param throwOriginal true to rethrow original exceptions, false to wrap them in MigrationErrorException + */ + void setThrowOriginal(boolean throwOriginal) { + this.throwOriginal = throwOriginal + } +} diff --git a/src/main/groovy/com/netgrif/application/engine/migration/throwable/MigrationErrorException.groovy b/src/main/groovy/com/netgrif/application/engine/migration/throwable/MigrationErrorException.groovy new file mode 100644 index 00000000000..47d1dca9814 --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/migration/throwable/MigrationErrorException.groovy @@ -0,0 +1,39 @@ +package com.netgrif.application.engine.migration.throwable + +import com.netgrif.application.engine.migration.model.MigrationError + +/** + * Exception thrown when one or more migration errors occur during the migration process. + * <p> + * This exception extends {@link RuntimeException} and encapsulates a list of {@link MigrationError} + * objects that provide detailed information about what went wrong during migration. + * The error list is immutable once the exception is created. + * </p> + */ +class MigrationErrorException extends RuntimeException { + + private final List<MigrationError> errors + + /** + * Constructs a new MigrationErrorException with the specified detail message, list of errors, and cause. + * + * @param message the detail message describing the overall migration failure + * @param errors the list of {@link MigrationError} objects detailing individual migration errors; + * if null or empty, an empty unmodifiable list will be used + * @param cause the cause of this exception (a null value is permitted and indicates that the cause + * is nonexistent or unknown); defaults to null if not specified + */ + MigrationErrorException(String message, List<MigrationError> errors, Throwable cause = null) { + super(message, cause) + this.errors = Collections.unmodifiableList(errors ?: []) + } + + /** + * Returns an unmodifiable list of migration errors that occurred. + * + * @return an unmodifiable {@link List} of {@link MigrationError} objects; never null but may be empty + */ + List<MigrationError> getErrors() { + return errors + } +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java index aeba1845e06..7716fec4609 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java @@ -8,6 +8,14 @@ import java.util.LinkedHashSet; import java.util.Set; + +/** + * Configuration properties class for managing migration-related settings in the application. + * This class is bound to the configuration prefix "nae.migration" and provides various options + * to control the behavior of migration processes, including skipping specific migrations, + * cache eviction, and automatic shutdown after migration completion. + * It also contains nested configuration classes for entity-specific migration settings. + */ @Data @Configuration @ConfigurationProperties(prefix = "nae.migration") @@ -36,4 +44,125 @@ public class MigrationProperties { */ private boolean shutdownAfterMigration = false; + /** + * Configuration properties specific to case migration. + * Contains settings that control how cases are migrated, including pagination options. + */ + private CaseMigrationProperties cases = new CaseMigrationProperties(); + + /** + * Configuration properties specific to task migration. + * Contains settings that control how tasks are migrated, including pagination options. + */ + private TaskMigrationProperties tasks = new TaskMigrationProperties(); + + /** + * Configuration properties specific to Petri net migration. + * Contains settings that control how Petri nets are migrated, including pagination options. + */ + private PetriNetMigrationProperties petriNets = new PetriNetMigrationProperties(); + + /** + * Default error handling policy used by migration helpers. + */ + private ErrorPolicy errorPolicy = new ErrorPolicy(); + + /** + * Configuration properties for case-specific migration settings. + * This nested configuration class allows fine-tuning of the case migration process. + */ + @Data + public static class CaseMigrationProperties { + + /** + * The number of cases to process in a single page during migration. + * This controls the batch size for paginated case migration operations. + * Default value is {@code 100}. + */ + private int pageSize = 100; + } + + /** + * Configuration properties for task-specific migration settings. + * This nested configuration class allows fine-tuning of the task migration process. + */ + @Data + public static class TaskMigrationProperties { + + /** + * The number of tasks to process in a single page during migration. + * This controls the batch size for paginated task migration operations. + * Default value is {@code 100}. + */ + private int pageSize = 100; + } + + /** + * Configuration properties for Petri net-specific migration settings. + * This nested configuration class allows fine-tuning of the Petri net migration process. + */ + @Data + public static class PetriNetMigrationProperties { + + /** + * The number of Petri nets to process in a single page during migration. + * This controls the batch size for paginated Petri net migration operations. + * Default value is {@code 100}. + */ + private int pageSize = 100; + } + + /** + * Configuration properties for error handling policy during migration operations. + * This nested configuration class defines how errors encountered during migration helper execution + * should be handled, including whether to throw exceptions immediately, continue processing, + * or apply error thresholds before terminating the migration process. + */ + @Data + public static class ErrorPolicy { + + /** + * Defines the error handling mode for migration helper operations. + * This property controls the behavior when errors are encountered during migration. + * <p> + * Supported values: + * <ul> + * <li><b>THROW_IMMEDIATELY</b> - Throws an exception as soon as the first error occurs, halting migration immediately.</li> + * <li><b>CONTINUE</b> - Continues processing despite errors, logging them without interrupting the migration flow.</li> + * <li><b>THROW_AFTER_LIMIT</b> - Continues processing until the number of errors reaches the threshold specified by {@code maxErrors}, then throws an exception.</li> + * <li><b>THROW_AFTER_PROCESSING</b> - Completes the entire migration process and throws an exception at the end if any errors were encountered.</li> + * </ul> + * Default value is {@code "CONTINUE"}. + */ + private String mode = "CONTINUE"; + + /** + * The maximum number of errors allowed before throwing an exception during migration. + * This property is only applicable when the {@code mode} is set to {@code THROW_AFTER_LIMIT}. + * When the number of encountered errors reaches this threshold, an exception will be thrown + * to halt further processing. A value of {@code 0} means no limit is enforced (though this + * effectively makes THROW_AFTER_LIMIT behave like THROW_AFTER_PROCESSING). + * Default value is {@code 0}. + */ + private int maxErrors = 0; + + /** + * Indicates whether errors encountered during migration should be cached for later analysis or processing. + * When enabled, all migration helper errors are stored in memory, allowing developers to review + * and analyze the errors after the migration completes. This is particularly useful for debugging + * and post-migration validation, as it provides a complete error history without interrupting the migration flow. + * Default value is {@code true}. + */ + private boolean cacheErrors = true; + + /** + * Indicates whether the original exception should be rethrown instead of a wrapped exception. + * When set to {@code true}, the migration framework will attempt to rethrow the original exception + * that occurred during helper execution, preserving the original stack trace and exception type. + * When {@code false}, exceptions may be wrapped in a migration-specific exception type. + * This property is useful for maintaining exception transparency and facilitating easier debugging. + * Default value is {@code false}. + */ + private boolean throwOriginal = false; + } } diff --git a/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy b/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy index b5cb8f9ac8c..53c27a30b1c 100644 --- a/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy @@ -2,14 +2,21 @@ package com.netgrif.application.engine.migration import com.netgrif.application.engine.TestHelper import com.netgrif.application.engine.migration.helpers.CaseMigrationHelper +import com.netgrif.application.engine.migration.model.MigrationError +import com.netgrif.application.engine.migration.model.MigrationErrorPolicy +import com.netgrif.application.engine.migration.throwable.MigrationErrorException import com.netgrif.application.engine.petrinet.domain.PetriNet import com.netgrif.application.engine.petrinet.domain.VersionType +import com.netgrif.application.engine.petrinet.domain.roles.ProcessRole import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService import com.netgrif.application.engine.startup.SuperCreator import com.netgrif.application.engine.workflow.domain.Case import com.netgrif.application.engine.workflow.domain.DataField import com.netgrif.application.engine.workflow.domain.QCase +import com.netgrif.application.engine.workflow.domain.QTask +import com.netgrif.application.engine.workflow.domain.Task import com.netgrif.application.engine.workflow.domain.eventoutcomes.petrinetoutcomes.ImportPetriNetEventOutcome +import com.netgrif.application.engine.workflow.service.interfaces.ITaskService import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService import groovy.util.logging.Slf4j import org.junit.jupiter.api.BeforeEach @@ -17,10 +24,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable; import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.junit.jupiter.SpringExtension +import static org.junit.jupiter.api.Assertions.assertThrows + @Slf4j @SpringBootTest @ActiveProfiles(["test"]) @@ -40,7 +50,7 @@ class MigrationTest { private IWorkflowService workflowService @Autowired - private CaseMigrationHelper caseMigrationHelper + private ITaskService taskService @Autowired private MigrationHelper migrationHelper @@ -51,13 +61,13 @@ class MigrationTest { void beforeEach() { testHelper.truncateDbs() - this.class.classLoader.getResourceAsStream("nae_2432_v1.xml").withCloseable { is -> + this.class.classLoader.getResourceAsStream("petriNets/nae_2432_v1.xml").withCloseable { is -> ImportPetriNetEventOutcome netV1Outcome = petriNetService.importPetriNet(is, VersionType.MAJOR, superCreator.getLoggedSuper()) assert netV1Outcome.getNet() != null netV1 = netV1Outcome.getNet() } - this.class.classLoader.getResourceAsStream("nae_2432_v2.xml").withCloseable { is -> + this.class.classLoader.getResourceAsStream("petriNets/nae_2432_v2.xml").withCloseable { is -> ImportPetriNetEventOutcome netV2Outcome = petriNetService.importPetriNet(is, VersionType.MAJOR, superCreator.getLoggedSuper()) assert netV2Outcome.getNet() != null netV2 = netV2Outcome.getNet() @@ -69,30 +79,258 @@ class MigrationTest { } @Test - void migrateCasesWithCursor() { - List<Case> caseList = workflowService.search(QCase.case$.processIdentifier.eq("nae_2432"), Pageable.ofSize(10)).getContent() - caseList.forEach { - assert !it.dataSet.containsKey("income") - assert !it.dataSet.containsKey("recreate_info_text") - assert it.enabledRoles.size() == 0 - assert it.tasks.size() == 1 && it.tasks[0].transition == "person_info" - } - - caseMigrationHelper.updateCasesCursor({ Case useCase -> - migrationHelper.updateCasePermissionsFromNet(useCase, netV2) - migrationHelper.updateTasksPermissions(useCase, netV2, ["t1", "t2"]) - migrationHelper.reloadTasks(useCase, netV2) - - useCase.dataSet["income"] = new DataField(0) - useCase.dataSet["recreate_info_text"] = new DataField("") - - }, "nae_2432") - caseList = workflowService.search(QCase.case$.processIdentifier.eq("nae_2432"), Pageable.ofSize(10)).getContent() - caseList.forEach { - assert it.dataSet.containsKey("income") - assert it.dataSet.containsKey("recreate_info_text") - assert it.enabledRoles.size() == 5 - assert it.tasks.size() == 2 && it.tasks.any {it.transition == "person_info"} && it.tasks.any {it.transition == "recreate_person"} + void migrationHelperShouldMigrateCasesAndReloadTasksThroughFacade() { + List<Case> casesBeforeMigration = workflowService.search( + QCase.case$.processIdentifier.eq("nae_2432"), + Pageable.ofSize(10) + ).content + + assert casesBeforeMigration.size() == 10 + casesBeforeMigration.each { Case useCase -> + assert !useCase.dataSet.containsKey("income") + assert !useCase.dataSet.containsKey("recreate_info_text") + assert useCase.enabledRoles.isEmpty() + assert useCase.tasks.size() == 1 + assert useCase.tasks[0].transition == "person_info" + } + + migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwAfterProcessing()) { + migrationHelper.updateCasesCursor({ Case useCase -> + migrationHelper.updateCasePermissionsFromNet(useCase, netV2) + migrationHelper.reloadTasks(useCase, netV2) + MigrationHelper.migratePetriNet(useCase, netV2) + + MigrationHelper.addTextDataFields(useCase, [ + "recreate_info_text": "" + ]) + useCase.dataSet["income"] = new DataField(1000) + }, "nae_2432", 2) + } + + List<Case> casesAfterMigration = workflowService.search( + QCase.case$.processIdentifier.eq("nae_2432"), + Pageable.ofSize(10) + ).content + + assert casesAfterMigration.size() == 10 + casesAfterMigration.each { Case useCase -> + assert useCase.petriNetObjectId == netV2.objectId + assert useCase.dataSet.containsKey("income") + assert useCase.dataSet["income"].value == 1000 + assert useCase.dataSet.containsKey("recreate_info_text") + assert useCase.enabledRoles.size() == 5 + assert useCase.tasks.size() == 2 + assert useCase.tasks.any { it.transition == "person_info" } + assert useCase.tasks.any { it.transition == "recreate_person" } + } + + assert !migrationHelper.hasErrors() + } + + @Test + void migrationHelperShouldUpdatePetriNetAndApplyCustomTransitionRoleUpdate() { + ProcessRole role = migrationHelper.createRoleInNet( + "nae_2432", + "migration_supervisor", + "Migration supervisor" + ) + + Closure<PetriNet> updateTransitionRole = migrationHelper.updateTransitionRolesClosure( + "person_info", + "migration_supervisor", + [ + view : true, + perform: true + ] + ) + + migrationHelper.updateNetIgnoreRoles("nae_2432", "nae_2432_v2.xml", [updateTransitionRole]) + + PetriNet migratedNet = petriNetService.getNewestVersionByIdentifier("nae_2432") + + assert migratedNet.dataSet.containsKey("income") + assert migratedNet.dataSet.containsKey("recreate_info_text") + assert migratedNet.transitions.values().any { it.importId == "recreate_person" } + + ProcessRole migratedRole = migratedNet.roles.values().find { + it.importId == "migration_supervisor" + } + assert migratedRole != null + + def personInfoTransition = migratedNet.transitions.values().find { + it.importId == "person_info" + } + assert personInfoTransition != null + assert personInfoTransition.roles[migratedRole.stringId]["view"] + assert personInfoTransition.roles[migratedRole.stringId]["perform"] + } + + @Test + void migrationHelperShouldUpdateTasksAndAddRoleToExistingTasks() { + ProcessRole role = migrationHelper.createRoleInNet( + "nae_2432", + "migration_task_role", + "Migration task role" + ) + + migrationHelper.addRoleToExistingTasks( + role, + netV1, + ["person_info"], + [ + view : true, + perform: true + ] + ) + + Page<Case> casePage = workflowService.search( + QCase.case$.processIdentifier.eq("nae_2432"), + Pageable.ofSize(10) + ) + + assert casePage.content.size() == 10 + + casePage.content.each { Case useCase -> + useCase.tasks.each { taskPair -> + if (taskPair.transition == "person_info") { + Task task = taskService.findOne(taskPair.task) + assert task.roles.containsKey(role.stringId) + assert task.roles[role.stringId]["view"] + assert task.roles[role.stringId]["perform"] + } + } + } + + migrationHelper.updateTasks( + { Task task -> + task.title.defaultValue = "Migrated task" + }, + QTask.task.transitionId.eq("person_info") + ) + + casePage.content.each { Case useCase -> + useCase.tasks.each { taskPair -> + if (taskPair.transition == "person_info") { + Task task = taskService.findOne(taskPair.task) + assert task.title.defaultValue == "Migrated task" + } + } + } + } + + @Test + void migrationHelperShouldCollectErrorsAndContinueMigration() { + migrationHelper.clearErrors() + + migrationHelper.withErrorPolicy(MigrationErrorPolicy.continueOnError()) { + migrationHelper.updateAllCasesCursor({ Case useCase -> + if (useCase.title.endsWith("1") || useCase.title.endsWith("2")) { + throw new IllegalStateException("Expected migration error for ${useCase.stringId}") + } + + useCase.title = "Successfully migrated" + }, 1) + } + + assert migrationHelper.hasErrors() + + List<MigrationError> errors = migrationHelper.popErrors() + assert errors.size() == 2 + assert errors.every { it.message.contains("Failed to prepare migration operation") } + assert !migrationHelper.hasErrors() + + List<Case> cases = workflowService.search( + QCase.case$.processIdentifier.eq("nae_2432"), + Pageable.ofSize(10) + ).content + + assert cases.count { it.title == "Successfully migrated" } == 8 + } + + @Test + void migrationHelperCollectErrorsShouldClearCacheBeforeAndAfterCollection() { + migrationHelper.clearErrors() + int allCases = workflowService.searchAll(QCase.case$._id.isNotNull()).getContent().size() + + List<MigrationError> errors = migrationHelper.collectErrors { + migrationHelper.withErrorPolicy(MigrationErrorPolicy.continueOnError()) { + migrationHelper.updateAllCasesCursor({ Case useCase -> + throw new IllegalStateException("Expected collected error") + }, 1) + } + } + + assert errors.size() == allCases + assert !migrationHelper.hasErrors() + assert errors.every { it.cause instanceof IllegalStateException } + } + + @Test + void updateNetIgnoreRolesShouldMigrateExistingNet() { + migrationHelper.updateNetIgnoreRoles("nae_2432", "nae_2432_v2.xml") + + def net = petriNetService.getNewestVersionByIdentifier("nae_2432") + + assert net.dataSet.containsKey("income") + assert net.transitions.values().any { it.importId == "recreate_person" } + } + + @Test + void throwImmediately() { + migrationHelper.clearErrors() + + assertThrows(MigrationErrorException) { + migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwImmediately()) { + migrationHelper.updateAllCasesCursor({ Case useCase -> + throw new IllegalStateException("Expected test error") + }, 1) + } + } + + assert migrationHelper.hasErrors() + } + + @Test + void throwAfterLimitIsReached() { + migrationHelper.clearErrors() + + def exception = assertThrows(MigrationErrorException) { + migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwAfterLimit(2)) { + migrationHelper.updateAllCasesCursor({ Case useCase -> + throw new IllegalStateException("Expected test error") + }, 1) + } } + + assert exception.errors.size() >= 2 + } + + @Test + void throwAfterProcessingFinished() { + migrationHelper.clearErrors() + + def exception = assertThrows(MigrationErrorException) { + migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwAfterProcessing()) { + migrationHelper.updateAllCasesCursor({ Case useCase -> + throw new IllegalStateException("Expected test error") + }, 1) + } + } + + assert exception.errors.size() > 0 + } + + @Test + void continueOnError() { + migrationHelper.clearErrors() + + migrationHelper.withErrorPolicy(MigrationErrorPolicy.continueOnError()) { + migrationHelper.updateAllCasesCursor({ Case useCase -> + throw new IllegalStateException("Expected test error") + }, 1) + } + + assert migrationHelper.hasErrors() + assert migrationHelper.popErrors().size() > 0 } } diff --git a/src/test/resources/petriNets/nae_2432_v1.xml b/src/test/resources/petriNets/nae_2432_v1.xml new file mode 100644 index 00000000000..fc98584acb4 --- /dev/null +++ b/src/test/resources/petriNets/nae_2432_v1.xml @@ -0,0 +1,257 @@ +<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.com/petriflow.schema.xsd"> + <id>nae_2432</id> + <version>1.0.0</version> + <initials>NAE</initials> + <title>NAE-2432 + mic + true + true + false + NAE-2432 + + delete_info_text + + <init><h1>Finish this task to delete person.</h1></init> + </data> + <data type="text"> + <id>first_name</id> + <title>First name + John + First name of person + + + last_name + Last name + Doe + Last name of person + + + note + Note + Example note + Notes about this person + + + reset_info_text + + <init><h1>Finish task to reset this person.</h1></init> + </data> + <transition> + <id>delete_person</id> + <x>816</x> + <y>176</y> + <label>Delete person</label> + <icon>delete</icon> + <dataGroup> + <id>delete_person_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>delete_info_text</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>htmltextarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>person_info</id> + <x>528</x> + <y>176</y> + <label>Person info</label> + <icon>person</icon> + <dataGroup> + <id>t1_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>first_name</id> + <logic> + <behavior>editable</behavior> + <behavior>required</behavior> + <behavior>immediate</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>last_name</id> + <logic> + <behavior>editable</behavior> + <behavior>required</behavior> + <behavior>immediate</behavior> + </logic> + <layout> + <x>2</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>note</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>1</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>textarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>reset_person</id> + <x>816</x> + <y>304</y> + <label>Reset person</label> + <icon>reset_tv</icon> + <dataGroup> + <id>reset_person_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>reset_info_text</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>htmltextarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <place> + <id>p1</id> + <x>336</x> + <y>176</y> + <tokens>1</tokens> + <static>false</static> + </place> + <place> + <id>p2</id> + <x>656</x> + <y>176</y> + <tokens>0</tokens> + <static>false</static> + </place> + <place> + <id>p3</id> + <x>656</x> + <y>304</y> + <tokens>0</tokens> + <static>false</static> + </place> + <place> + <id>p4</id> + <x>976</x> + <y>176</y> + <tokens>1</tokens> + <static>false</static> + </place> + <arc> + <id>a1</id> + <type>regular</type> + <sourceId>p1</sourceId> + <destinationId>person_info</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a2</id> + <type>regular</type> + <sourceId>person_info</sourceId> + <destinationId>p2</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a3</id> + <type>regular</type> + <sourceId>person_info</sourceId> + <destinationId>p3</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a4</id> + <type>regular</type> + <sourceId>p2</sourceId> + <destinationId>delete_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a5</id> + <type>regular</type> + <sourceId>p3</sourceId> + <destinationId>reset_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a6</id> + <type>regular</type> + <sourceId>delete_person</sourceId> + <destinationId>p4</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a7</id> + <type>regular</type> + <sourceId>reset_person</sourceId> + <destinationId>p1</destinationId> + <multiplicity>1</multiplicity> + <breakpoint> + <x>816</x> + <y>368</y> + </breakpoint> + <breakpoint> + <x>336</x> + <y>368</y> + </breakpoint> + </arc> + <arc> + <id>a8</id> + <type>regular</type> + <sourceId>p2</sourceId> + <destinationId>reset_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a9</id> + <type>regular</type> + <sourceId>p3</sourceId> + <destinationId>delete_person</destinationId> + <multiplicity>1</multiplicity> + </arc> +</document> \ No newline at end of file diff --git a/src/test/resources/petriNets/nae_2432_v2.xml b/src/test/resources/petriNets/nae_2432_v2.xml new file mode 100644 index 00000000000..b1cab7bc21f --- /dev/null +++ b/src/test/resources/petriNets/nae_2432_v2.xml @@ -0,0 +1,388 @@ +<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.com/petriflow.schema.xsd"> + <id>nae_2432</id> + <version>1.0.0</version> + <initials>NAE</initials> + <title>NAE-2432 + mic + true + true + false + NAE-2432 + + data_editor + Data editor + + + person_creator + Person creator + + + person_recreator + Person recreator + + + person_remover + Person remover + + + person_reseter + Person resetter + + + delete_info_text + + <init><h1>Finish this task to delete person.</h1></init> + </data> + <data type="text"> + <id>first_name</id> + <title>First name + John + First name of person + + + income + Income + Income of person + 1000 + + + last_name + Last name + Doe + Last name of person + + + note + Note + Example note + Notes about this person + + + recreate_info_text + + <init><h1>Finish this task to recreate person.</h1></init> + </data> + <data type="text"> + <id>reset_info_text</id> + <title/> + <init><h1>Finish task to reset this person.</h1></init> + </data> + <transition> + <id>delete_person</id> + <x>816</x> + <y>176</y> + <label>Delete person</label> + <icon>delete</icon> + <roleRef> + <id>person_remover</id> + <logic> + <view>true</view> + <perform>true</perform> + </logic> + </roleRef> + <dataGroup> + <id>delete_person_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>delete_info_text</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>htmltextarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>person_info</id> + <x>528</x> + <y>176</y> + <label>Person info</label> + <icon>person</icon> + <roleRef> + <id>data_editor</id> + <logic> + <view>true</view> + <perform>true</perform> + </logic> + </roleRef> + <roleRef> + <id>person_creator</id> + <logic> + <view>true</view> + <perform>true</perform> + </logic> + </roleRef> + <dataGroup> + <id>t1_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>first_name</id> + <logic> + <behavior>editable</behavior> + <behavior>required</behavior> + <behavior>immediate</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>last_name</id> + <logic> + <behavior>editable</behavior> + <behavior>required</behavior> + <behavior>immediate</behavior> + </logic> + <layout> + <x>2</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>income</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>1</y> + <rows>1</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>currency</name> + <property key="fractionSize">2</property> + </component> + </dataRef> + <dataRef> + <id>note</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>2</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>textarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>recreate_person</id> + <x>976</x> + <y>48</y> + <label>Recreate person</label> + <icon>emergency</icon> + <roleRef> + <id>person_recreator</id> + <logic> + <view>true</view> + <perform>true</perform> + </logic> + </roleRef> + <dataGroup> + <id>recreate_person_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>recreate_info_text</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>htmltextarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>reset_person</id> + <x>816</x> + <y>304</y> + <label>Reset person</label> + <icon>reset_tv</icon> + <roleRef> + <id>person_reseter</id> + <logic> + <view>true</view> + <perform>true</perform> + </logic> + </roleRef> + <dataGroup> + <id>reset_person_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>reset_info_text</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>2</rows> + <cols>4</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + <component> + <name>htmltextarea</name> + </component> + </dataRef> + </dataGroup> + </transition> + <place> + <id>p1</id> + <x>336</x> + <y>176</y> + <tokens>1</tokens> + <static>false</static> + </place> + <place> + <id>p2</id> + <x>656</x> + <y>176</y> + <tokens>0</tokens> + <static>false</static> + </place> + <place> + <id>p3</id> + <x>656</x> + <y>304</y> + <tokens>0</tokens> + <static>false</static> + </place> + <place> + <id>p4</id> + <x>976</x> + <y>176</y> + <tokens>0</tokens> + <static>false</static> + </place> + <arc> + <id>a1</id> + <type>regular</type> + <sourceId>p1</sourceId> + <destinationId>person_info</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a10</id> + <type>regular</type> + <sourceId>p4</sourceId> + <destinationId>recreate_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a11</id> + <type>regular</type> + <sourceId>recreate_person</sourceId> + <destinationId>p1</destinationId> + <multiplicity>1</multiplicity> + <breakpoint> + <x>336</x> + <y>48</y> + </breakpoint> + </arc> + <arc> + <id>a2</id> + <type>regular</type> + <sourceId>person_info</sourceId> + <destinationId>p2</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a3</id> + <type>regular</type> + <sourceId>person_info</sourceId> + <destinationId>p3</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a4</id> + <type>regular</type> + <sourceId>p2</sourceId> + <destinationId>delete_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a5</id> + <type>regular</type> + <sourceId>p3</sourceId> + <destinationId>reset_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a6</id> + <type>regular</type> + <sourceId>delete_person</sourceId> + <destinationId>p4</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a7</id> + <type>regular</type> + <sourceId>reset_person</sourceId> + <destinationId>p1</destinationId> + <multiplicity>1</multiplicity> + <breakpoint> + <x>816</x> + <y>368</y> + </breakpoint> + <breakpoint> + <x>336</x> + <y>368</y> + </breakpoint> + </arc> + <arc> + <id>a8</id> + <type>regular</type> + <sourceId>p2</sourceId> + <destinationId>reset_person</destinationId> + <multiplicity>1</multiplicity> + </arc> + <arc> + <id>a9</id> + <type>regular</type> + <sourceId>p3</sourceId> + <destinationId>delete_person</destinationId> + <multiplicity>1</multiplicity> + </arc> +</document> \ No newline at end of file From 8fd6f8127b6dc7cd03c315b2e46987c89f14a20a Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Fri, 29 May 2026 10:31:10 +0200 Subject: [PATCH 16/20] - updated according to PR suggestions --- .../migration_error_handling_quick_usage.md | 27 ++++--- .../engine/migration/MigrationHelper.groovy | 29 +++++-- .../helpers/AbstractMigrationHelper.groovy | 76 +++++++++---------- .../helpers/CaseMigrationHelper.groovy | 14 ++-- .../helpers/TaskMigrationHelper.groovy | 10 +-- .../model/MigrationErrorPolicy.groovy | 21 ++++- .../throwable/MigrationErrorException.groovy | 2 +- 7 files changed, 104 insertions(+), 75 deletions(-) diff --git a/docs/migration/migration_error_handling_quick_usage.md b/docs/migration/migration_error_handling_quick_usage.md index 8b5df28b837..fbf241a0e1b 100644 --- a/docs/migration/migration_error_handling_quick_usage.md +++ b/docs/migration/migration_error_handling_quick_usage.md @@ -1,10 +1,12 @@ -## Migration Error Handling — Quick Usage +# Migration Error Handling — Quick Usage + Use `MigrationErrorPolicy` to control what happens when a migration helper encounters an error. ``` groovy import com.netgrif.application.engine.migration.model.MigrationErrorPolicy ``` -### Available policies +## Available policies + ``` groovy MigrationErrorPolicy.continueOnError() // log/cache errors and continue MigrationErrorPolicy.throwImmediately() // stop on first error @@ -12,7 +14,8 @@ MigrationErrorPolicy.throwAfterLimit(10) // stop after 10 errors MigrationErrorPolicy.throwAfterProcessing() // process all, then fail if errors exist ``` -#### Basic usage +### Basic usage + ``` groovy migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwAfterProcessing()) { migrationHelper.updateAllCasesCursor({ Case useCase -> @@ -21,7 +24,8 @@ migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwAfterProcessing()) { } ``` -#### Continue migration and inspect errors +### Continue migration and inspect errors + ``` groovy migrationHelper.clearErrors() @@ -38,8 +42,8 @@ errors.each { error -> } ``` +### Stop after a number of errors -#### Stop after a number of errors ``` groovy migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwAfterLimit(20)) { migrationHelper.updateAllTasksCursor({ Task task -> @@ -48,8 +52,8 @@ migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwAfterLimit(20)) { } ``` - -#### Fail after full processing +### Fail after full processing + ``` groovy import com.netgrif.application.engine.migration.throwable.MigrationErrorException @@ -70,8 +74,8 @@ try { } ``` +### Error cache helpers -#### Error cache helpers ``` groovy migrationHelper.clearErrors() // clears cached errors migrationHelper.hasErrors() // true if errors exist @@ -79,8 +83,8 @@ migrationHelper.getErrors() // returns errors without clearing migrationHelper.popErrors() // returns errors and clears cache ``` - -#### Recommended production pattern +### Recommended production pattern + ``` groovy migrationHelper.clearErrors() @@ -88,5 +92,4 @@ migrationHelper.withErrorPolicy(MigrationErrorPolicy.throwAfterProcessing()) { // migration logic } ``` - -Use throwAfterProcessing() for most production migrations because it collects a full error report and fails only after all possible records are processed. \ No newline at end of file +Use throwAfterProcessing() for most production migrations because it collects a full error report and fails only after all possible records are processed. diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy index c8d6f684cea..7b07baaff7d 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy @@ -132,7 +132,7 @@ class MigrationHelper { * @param sleepFor Optional attribute to set sleep time (in milliseconds) to sleep for after each iterated page. Default 0ms * @param filter Instance of Predicate, to filter which cases should be iterated */ - void iterateCases(Closure update, Closure pageProcessed = AbstractMigrationHelper.DEFAULT_PROCESS_OPERATIONS, + void iterateCases(Closure update, Closure pageProcessed = null, long sleepFor = 0, Predicate filter) { log.debug("iterateCases called with filter: {}, sleepFor: {}", filter, sleepFor) caseMigrationHelper.iterateCases(update, pageProcessed, sleepFor, filter, getCurrentErrorPolicy()) @@ -175,7 +175,7 @@ class MigrationHelper { * @param sleepFor Optional attribute to set sleep time (in milliseconds) to sleep for after each iterated page. Default 0ms * @param filter Instance of Predicate, to filter which tasks should be iterated */ - void iterateTasks(Closure update, Closure pageProcessed = AbstractMigrationHelper.DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter) { + void iterateTasks(Closure update, Closure pageProcessed = null, long sleepFor = 0, Predicate filter) { log.debug("iterateTasks called with filter: {}, sleepFor: {}", filter, sleepFor) taskMigrationHelper.iterateTasks(update, pageProcessed, sleepFor, filter, getCurrentErrorPolicy()) } @@ -533,7 +533,11 @@ class MigrationHelper { * @return immutable snapshot of cached migration errors */ List<MigrationError> getErrors() { - return AbstractMigrationHelper.getErrors() + List<MigrationError> errors = [] + errors.addAll(caseMigrationHelper.getErrors()) + errors.addAll(taskMigrationHelper.getErrors()) + errors.addAll(petriNetMigrationHelper.getErrors()) + return Collections.unmodifiableList(errors) } /** @@ -542,14 +546,20 @@ class MigrationHelper { * @return cached migration errors collected since the last clear/pop */ List<MigrationError> popErrors() { - return AbstractMigrationHelper.popErrors() + List<MigrationError> errors = [] + errors.addAll(caseMigrationHelper.popErrors()) + errors.addAll(taskMigrationHelper.popErrors()) + errors.addAll(petriNetMigrationHelper.popErrors()) + return errors } /** * Clears cached migration errors. */ void clearErrors() { - AbstractMigrationHelper.clearErrors() + caseMigrationHelper.clearErrors() + taskMigrationHelper.clearErrors() + petriNetMigrationHelper.clearErrors() } /** @@ -558,7 +568,7 @@ class MigrationHelper { * @return true if at least one error is cached */ boolean hasErrors() { - return AbstractMigrationHelper.hasErrors() + return caseMigrationHelper.hasErrors() || taskMigrationHelper.hasErrors() || petriNetMigrationHelper.hasErrors() } /** @@ -569,7 +579,10 @@ class MigrationHelper { */ List<MigrationError> collectErrors(Closure migrationCode) { clearErrors() - migrationCode.call() - return popErrors() + try { + migrationCode.call() + } finally { + return popErrors() + } } } \ No newline at end of file diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy index eca0b860817..d116c7901cb 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy @@ -29,13 +29,6 @@ import java.util.concurrent.atomic.AtomicInteger @Slf4j abstract class AbstractMigrationHelper<T> { - /** - * Default Closure used to process bulk operations. It uses the {@link #handleBulkOps} method - * to safely execute the bulk operations and log results or errors. - */ - static final Closure DEFAULT_PROCESS_OPERATIONS = { BulkOperations bulkOperations, Class<?> type -> handleBulkOps(bulkOperations, type) } - - /** * The type of the documents this helper is operating on. * It is expected to be provided by subclasses, as the class itself is generic and requires @@ -62,7 +55,7 @@ abstract class AbstractMigrationHelper<T> { * and contains a list of {@link MigrationError} objects representing all errors associated with that key. * This structure allows for efficient error tracking and reporting during bulk migration operations. */ - private static final List<MigrationError> MIGRATION_ERRORS = new CopyOnWriteArrayList<>() + private final List<MigrationError> migrationErrors /** * Constructs a new AbstractMigrationHelper with the specified MongoTemplate. @@ -75,6 +68,7 @@ abstract class AbstractMigrationHelper<T> { this.type = type this.mongoTemplate = mongoTemplate this.migrationProperties = migrationProperties + this.migrationErrors = new CopyOnWriteArrayList<>() } /** @@ -118,13 +112,13 @@ abstract class AbstractMigrationHelper<T> { * @param message a descriptive message explaining the error * @param cause the optional {@link Throwable} that caused the error; defaults to null */ - static void cacheError(String helper, + void cacheError(String helper, String operation, Class<?> entityType, String entityId, String message, Throwable cause = null) { - MIGRATION_ERRORS.add(MigrationError.of(helper, operation, entityType, entityId, message, cause)) + migrationErrors.add(MigrationError.of(helper, operation, entityType, entityId, message, cause)) } /** @@ -134,50 +128,52 @@ abstract class AbstractMigrationHelper<T> { * * @return an unmodifiable {@link List} of {@link MigrationError} objects */ - static List<MigrationError> getErrors() { - return Collections.unmodifiableList(new ArrayList<>(MIGRATION_ERRORS)) + List<MigrationError> getErrors() { + return Collections.unmodifiableList(new ArrayList<>(migrationErrors)) } /** - * Retrieves all cached migration errors and clears the error cache in a single operation. + * Retrieves all cached migration errors from this helper instance and clears the error cache. * This method is useful for retrieving errors for reporting purposes while simultaneously * resetting the error cache for a new migration operation. * * @return a {@link List} of all {@link MigrationError} objects that were cached */ - static List<MigrationError> popErrors() { - List<MigrationError> errors = new ArrayList<>(MIGRATION_ERRORS) - MIGRATION_ERRORS.clear() - return errors + List<MigrationError> popErrors() { + synchronized (migrationErrors) { + List<MigrationError> errors = new ArrayList<>(migrationErrors) + migrationErrors.clear() + return errors + } } - /** - * Clears all cached migration errors from the error list. - * This method should be called to reset the error cache before starting a new migration - * operation or after errors have been processed and reported. - */ - static void clearErrors() { - MIGRATION_ERRORS.clear() +/** + * Clears all cached migration errors from this helper instance. + * This method should be called to reset this helper's error cache before starting a new migration + * operation or after errors have been processed and reported. + */ + void clearErrors() { + migrationErrors.clear() } /** - * Checks whether any migration errors have been cached. + * Checks whether any migration errors have been cached by this helper instance. * This method is useful for quickly determining if any errors occurred during * the migration process without retrieving the full error list. * * @return {@code true} if one or more errors are cached, {@code false} otherwise */ - static boolean hasErrors() { - return !MIGRATION_ERRORS.isEmpty() + boolean hasErrors() { + return !migrationErrors.isEmpty() } /** - * A static method to handle the execution of bulk operations. + * Handles the execution of bulk operations. * It executes the given {@link BulkOperations} instance and logs the results or any errors. * * @param bulkOps the bulk operations to execute */ - static void handleBulkOps(BulkOperations bulkOps, Class<?> type) { + void handleBulkOps(BulkOperations bulkOps, Class<?> type) { try { BulkWriteResult bulkWriteResult = bulkOps.execute() log.debug("Processed bulk write of ${bulkWriteResult.modifiedCount}") @@ -186,30 +182,33 @@ abstract class AbstractMigrationHelper<T> { e.getWriteErrors().forEach { String message = "Error writing document with ID ${it.toString()}. Cause: ${it.getMessage()}" log.error(message) - cacheError(AbstractMigrationHelper.simpleName, "bulkWrite", type, it.toString(), message, e) + cacheError(this.class.simpleName, "bulkWrite", type, it.toString(), message, e) } throw e } } /** - * Iterates over the documents in the collection, applies updates, and executes bulk operations. - * The iteration is paginated based on the provided or default page size, and supports customizable + Iterates over the documents in the collection, applies updates, and executes bulk operations. * The iteration is paginated based on the provided or default page size, and supports customizable * bulk operation processing and optional sleep intervals between pages. * * @param update a {@link Closure} defining the update to apply to documents * @param processOperations an optional {@link Closure} to process bulk operations; defaults - * to {@link #DEFAULT_PROCESS_OPERATIONS} + * to null * @param query an optional MongoDB {@link Query} to filter documents; defaults to an empty query * @param sleepFor the optional number of milliseconds to sleep between processing pages; defaults to 0 * @param pageSize the size of each page (number of documents); defaults to the result of {@link #getPageSize()} */ - void iterate(Closure update, Closure processOperations = DEFAULT_PROCESS_OPERATIONS, + void iterate(Closure update, Closure processOperations = null, Query query = new Query(), long sleepFor = 0, int pageSize = getPageSize(), MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { if (pageSize <= 0) { throw new IllegalArgumentException("pageSize must be > 0") } + Closure effectiveProcessOperations = processOperations ?: { BulkOperations bulkOperations, Class<?> entityType -> + handleBulkOps(bulkOperations, entityType) + } + long count = mongoTemplate.count(query, type) if (count > 0) { long numOfPages = Math.ceil(count / pageSize) as long @@ -238,13 +237,12 @@ abstract class AbstractMigrationHelper<T> { try { if (currentBulkOpsSize > 0) { - processOperations(bulkOps, type) + effectiveProcessOperations(bulkOps, type) } } catch (Exception e) { String message = "Failed to process ${type.simpleName} bulk operations on page ${page}" log.error(message, e) - cacheError(this.class.simpleName, "iterate", type, null, message, e) - } + handleMigrationError(errorPolicy, "bulkWrite", type, null, message, e) } bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, type) currentBatchSize = 0 @@ -337,7 +335,7 @@ abstract class AbstractMigrationHelper<T> { * @param cause the optional {@link Throwable} that caused the error; defaults to null * @throws RuntimeException or {@link MigrationErrorException} depending on the policy and cause */ - protected static void throwError(MigrationErrorPolicy policy, String message, Throwable cause = null) { + protected void throwError(MigrationErrorPolicy policy, String message, Throwable cause = null) { if (policy.throwOriginal && cause instanceof RuntimeException) { throw cause } @@ -359,7 +357,7 @@ abstract class AbstractMigrationHelper<T> { * @param policy the {@link MigrationErrorPolicy} defining the error handling behavior * @throws MigrationErrorException if the policy requires throwing after processing and errors exist */ - protected static void finishMigrationErrorPolicy(MigrationErrorPolicy policy) { + protected void finishMigrationErrorPolicy(MigrationErrorPolicy policy) { if (policy.mode == MigrationErrorHandlingMode.THROW_AFTER_PROCESSING && hasErrors()) { throw new MigrationErrorException( "Migration finished with ${getErrors().size()} errors", diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index 33733ea3c4f..45454af2faa 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -119,7 +119,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { */ void updateCases(Closure update, Predicate filter, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Updating cases with filter ${filter.toString()} and update ${update.toString()}") - iterate(update, DEFAULT_PROCESS_OPERATIONS, toQuery(filter), 0, getPageSize(), errorPolicy) + iterate(update, null, toQuery(filter), 0, getPageSize(), errorPolicy) } /** @@ -127,11 +127,11 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * is executed for each matched case, and the pageProcessed closure is called after each page. * * @param update A closure containing the code to execute for each matching case. - * @param pageProcessed A closure executed after processing each page. Defaults to DEFAULT_PROCESS_OPERATIONS. + * @param pageProcessed A closure executed after processing each page. Defaults to null. * @param sleepFor Optional sleep time (in milliseconds) between processing pages. Default is 0ms. * @param filter A QueryDSL Predicate object specifying the conditions to filter the cases. */ - void iterateCases(Closure update, Closure pageProcessed = DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, + void iterateCases(Closure update, Closure pageProcessed = null, long sleepFor = 0, Predicate filter, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting iterateCases with filter: ${filter.toString()}, sleepFor: ${sleepFor}ms") iterate(update, pageProcessed, toQuery(filter), sleepFor, getPageSize(), errorPolicy) @@ -148,7 +148,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateCasesCursor for processIdentifier: ${processIdentifier}, pageSize: ${pageSize}") Query query = new Query(Criteria.where("processIdentifier").is(processIdentifier)) - iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int, errorPolicy) + iterate(update, null, query, 0, pageSize as int, errorPolicy) } /** @@ -159,7 +159,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { */ void updateAllCasesCursor(Closure update, int pageSize = 100, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateAllCasesCursor with pageSize: ${pageSize}") - iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int, errorPolicy) + iterate(update, null, new Query(), 0, pageSize as int, errorPolicy) } /** @@ -174,7 +174,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { if (!useCase.petriNet) { String message = "Failed to set petriNet for case $useCase.stringId" log.error(message) - cacheError(this.class.simpleName, "elasticIndex", type, useCase.stringId, message) + handleMigrationError(errorPolicy, "elasticIndex", type, useCase.stringId, message) return } log.trace("Successfully set petriNet for case: ${useCase.stringId}") @@ -188,7 +188,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { } catch (Exception retryEx) { String message = "Failed to index $useCase.stringId after setting lastModified" log.error(message, retryEx) - handleMigrationError(errorPolicy, "elasticIndex", type, useCase.stringId, message, ex) + handleMigrationError(errorPolicy, "elasticIndex", type, useCase.stringId, message, retryEx) } } else { String message = "Failed to index $useCase.stringId" diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy index 32f2c8e5c6a..ceada0c4f38 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy @@ -137,7 +137,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { log.debug("Starting updateTasks with filter: ${filter.toString()}") log.info("Updating tasks with filter ${filter.toString()} and update ${update.toString()}") log.trace("Converting filter to query and calling iterate") - iterate(update, DEFAULT_PROCESS_OPERATIONS, toQuery(filter), 0, getPageSize(), errorPolicy) + iterate(update, null, toQuery(filter), 0, getPageSize(), errorPolicy) } /** @@ -146,7 +146,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { * @param sleepFor Optional attribute to set sleep time (in milliseconds) to sleep for after each iterated page. Default 0ms * @param filter Instance of Predicate, to filter which tasks should be iterated */ - void iterateTasks(Closure update, Closure pageProcessed = DEFAULT_PROCESS_OPERATIONS, long sleepFor = 0, Predicate filter, + void iterateTasks(Closure update, Closure pageProcessed = null, long sleepFor = 0, Predicate filter, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting iterateTasks with filter: ${filter.toString()}, sleepFor: ${sleepFor}ms") log.trace("Converting filter to query and calling iterate with pageProcessed closure") @@ -165,7 +165,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { String processId = petriNetService.getNewestVersionByIdentifier(processIdentifier).stringId Query query = new Query(Criteria.where("processId").is(processId)) log.trace("Created query for processId: ${processId}, calling iterate") - iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int, errorPolicy) + iterate(update, null, query, 0, pageSize as int, errorPolicy) } /** @@ -182,7 +182,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { Query query = new Query(Criteria.where("processId").is(processId)) query.addCriteria(Criteria.where("transitionId").in(transitionIds)) log.trace("Created query with criteria for processId: ${processId} and transitionIds: ${transitionIds}, calling iterate") - iterate(update, DEFAULT_PROCESS_OPERATIONS, query, 0, pageSize as int, errorPolicy) + iterate(update, null, query, 0, pageSize as int, errorPolicy) } /** @@ -193,7 +193,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper<Task> { void updateAllTasksCursor(Closure update, int pageSize = 100, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateAllTasksCursor with pageSize: ${pageSize}") log.trace("Calling iterate with empty query to process all tasks") - iterate(update, DEFAULT_PROCESS_OPERATIONS, new Query(), 0, pageSize as int, errorPolicy) + iterate(update, null, new Query(), 0, pageSize as int, errorPolicy) } /** diff --git a/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationErrorPolicy.groovy b/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationErrorPolicy.groovy index 6334bf7b7c9..7763230ac3e 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationErrorPolicy.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/model/MigrationErrorPolicy.groovy @@ -1,6 +1,6 @@ package com.netgrif.application.engine.migration.model -import com.netgrif.application.engine.configuration.properties.MigrationProperties +import com.netgrif.application.engine.configuration.properties.MigrationProperties.ErrorPolicy /** * Configuration class that defines how errors should be handled during migration processes. @@ -52,9 +52,18 @@ class MigrationErrorPolicy { * @param migrationProperties the migration configuration properties containing error policy settings * @return a new MigrationErrorPolicy configured according to the application properties */ - static MigrationErrorPolicy defaultErrorPolicy(MigrationProperties.ErrorPolicy props) { + static MigrationErrorPolicy defaultErrorPolicy(ErrorPolicy props) { + if (props == null || props.mode == null || props.mode.trim().isEmpty()) { + return new MigrationErrorPolicy() + } + MigrationErrorHandlingMode parsedMode + try { + parsedMode = MigrationErrorHandlingMode.valueOf(props.mode.trim().toUpperCase(Locale.ROOT)) + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException("Invalid nae.migration.error-policy.mode '${props.mode}'. Supported values: ${MigrationErrorHandlingMode.values()*.name().join(', ')}", ex) + } return new MigrationErrorPolicy( - mode: MigrationErrorHandlingMode.valueOf(props.mode), + mode: parsedMode, maxErrors: props.maxErrors, cacheErrors: props.cacheErrors, throwOriginal: props.throwOriginal @@ -89,6 +98,9 @@ class MigrationErrorPolicy { * @return a new MigrationErrorPolicy configured to throw after reaching the error limit */ static MigrationErrorPolicy throwAfterLimit(int maxErrors) { + if (maxErrors <= 0) { + throw new IllegalArgumentException("maxErrors must be > 0 for THROW_AFTER_LIMIT") + } return new MigrationErrorPolicy( mode: MigrationErrorHandlingMode.THROW_AFTER_LIMIT, maxErrors: maxErrors @@ -139,6 +151,9 @@ class MigrationErrorPolicy { * @param maxErrors the maximum error count threshold */ void setMaxErrors(int maxErrors) { + if (maxErrors < 0) { + throw new IllegalArgumentException("maxErrors cannot be negative") + } this.maxErrors = maxErrors } diff --git a/src/main/groovy/com/netgrif/application/engine/migration/throwable/MigrationErrorException.groovy b/src/main/groovy/com/netgrif/application/engine/migration/throwable/MigrationErrorException.groovy index 47d1dca9814..b817dbedb8f 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/throwable/MigrationErrorException.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/throwable/MigrationErrorException.groovy @@ -25,7 +25,7 @@ class MigrationErrorException extends RuntimeException { */ MigrationErrorException(String message, List<MigrationError> errors, Throwable cause = null) { super(message, cause) - this.errors = Collections.unmodifiableList(errors ?: []) + this.errors = Collections.unmodifiableList(new ArrayList<>(errors ?: [])) } /** From 6e34cf4c12e903822f7b41659d124f0aad061b35 Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Fri, 29 May 2026 12:50:01 +0200 Subject: [PATCH 17/20] Make `changeDataFieldsValueFromTextToNumber` non-static and handle null DataField values in `CaseMigrationHelper`. --- .../application/engine/migration/MigrationHelper.groovy | 4 ++-- .../engine/migration/helpers/CaseMigrationHelper.groovy | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy index 7b07baaff7d..78759a86f49 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy @@ -404,8 +404,8 @@ class MigrationHelper { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - static void changeDataFieldsValueFromTextToNumber(Case useCase, Set<String> toChange) { - CaseMigrationHelper.changeDataFieldsValueFromTextToNumber(useCase, toChange) + void changeDataFieldsValueFromTextToNumber(Case useCase, Set<String> toChange) { + caseMigrationHelper.changeDataFieldsValueFromTextToNumber(useCase, toChange) } /** diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index 45454af2faa..339929872c4 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -233,7 +233,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { * @param useCase Instance of Case * @param toChange List of field IDs for value change */ - static void changeDataFieldsValueFromTextToNumber(Case useCase, Set<String> toChange, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { + void changeDataFieldsValueFromTextToNumber(Case useCase, Set<String> toChange, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting changeDataFieldsValueFromTextToNumber for case: ${useCase.stringId}, fields to change: ${toChange}") toChange.each { dataFieldID -> DataField dataField = useCase.dataSet[dataFieldID] @@ -275,6 +275,9 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { log.debug("Starting changeDataFieldsValueFromEnumerationToMultichoice for case: ${useCase.stringId}, fields to change: ${toChange}") toChange.each { dataFieldID -> DataField dataField = useCase.dataSet[dataFieldID] + if (!dataField) { + return + } if (dataField.value && dataField.value != null) { def value if (dataField.value instanceof I18nString) { From 2e2229de320fe9242efee14beecacb2e29bf6aa5 Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Fri, 29 May 2026 13:18:11 +0200 Subject: [PATCH 18/20] Pass `errorPolicy` explicitly in `changeDataFieldsValueFromTextToNumber` and simplify `pageSize` casting in `CaseMigrationHelper`. --- .../application/engine/migration/MigrationHelper.groovy | 2 +- .../engine/migration/helpers/CaseMigrationHelper.groovy | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy index 78759a86f49..654c5a0be2a 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationHelper.groovy @@ -405,7 +405,7 @@ class MigrationHelper { * @param toChange List of field IDs for value change */ void changeDataFieldsValueFromTextToNumber(Case useCase, Set<String> toChange) { - caseMigrationHelper.changeDataFieldsValueFromTextToNumber(useCase, toChange) + caseMigrationHelper.changeDataFieldsValueFromTextToNumber(useCase, toChange, getCurrentErrorPolicy()) } /** diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index 339929872c4..074bdd1cc92 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -148,7 +148,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateCasesCursor for processIdentifier: ${processIdentifier}, pageSize: ${pageSize}") Query query = new Query(Criteria.where("processIdentifier").is(processIdentifier)) - iterate(update, null, query, 0, pageSize as int, errorPolicy) + iterate(update, null, query, 0, pageSize, errorPolicy) } /** @@ -159,7 +159,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper<Case> { */ void updateAllCasesCursor(Closure update, int pageSize = 100, MigrationErrorPolicy errorPolicy = defaultErrorPolicy()) { log.debug("Starting updateAllCasesCursor with pageSize: ${pageSize}") - iterate(update, null, new Query(), 0, pageSize as int, errorPolicy) + iterate(update, null, new Query(), 0, pageSize, errorPolicy) } /** From b5272c5271e33e1d01ff7b4f3f36837ba892cc4f Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Mon, 1 Jun 2026 13:22:51 +0200 Subject: [PATCH 19/20] Rename and migrate test Petri net files for `MigrationTest`, updating identifiers and test assertions. --- .../engine/migration/MigrationTest.groovy | 26 +- .../migration_test_v1.xml} | 2 +- .../migration_test_v2.xml} | 2 +- src/test/resources/petriNets/nae_2432_v1.xml | 257 ------------ src/test/resources/petriNets/nae_2432_v2.xml | 388 ------------------ 5 files changed, 15 insertions(+), 660 deletions(-) rename src/test/resources/{nae_2432_v1.xml => petriNets/migration_test_v1.xml} (99%) rename src/test/resources/{nae_2432_v2.xml => petriNets/migration_test_v2.xml} (99%) delete mode 100644 src/test/resources/petriNets/nae_2432_v1.xml delete mode 100644 src/test/resources/petriNets/nae_2432_v2.xml diff --git a/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy b/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy index 53c27a30b1c..e6bc4220462 100644 --- a/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/migration/MigrationTest.groovy @@ -61,13 +61,13 @@ class MigrationTest { void beforeEach() { testHelper.truncateDbs() - this.class.classLoader.getResourceAsStream("petriNets/nae_2432_v1.xml").withCloseable { is -> + this.class.classLoader.getResourceAsStream("petriNets/migration_test_v1.xml").withCloseable { is -> ImportPetriNetEventOutcome netV1Outcome = petriNetService.importPetriNet(is, VersionType.MAJOR, superCreator.getLoggedSuper()) assert netV1Outcome.getNet() != null netV1 = netV1Outcome.getNet() } - this.class.classLoader.getResourceAsStream("petriNets/nae_2432_v2.xml").withCloseable { is -> + this.class.classLoader.getResourceAsStream("petriNets/migration_test_v2.xml").withCloseable { is -> ImportPetriNetEventOutcome netV2Outcome = petriNetService.importPetriNet(is, VersionType.MAJOR, superCreator.getLoggedSuper()) assert netV2Outcome.getNet() != null netV2 = netV2Outcome.getNet() @@ -81,7 +81,7 @@ class MigrationTest { @Test void migrationHelperShouldMigrateCasesAndReloadTasksThroughFacade() { List<Case> casesBeforeMigration = workflowService.search( - QCase.case$.processIdentifier.eq("nae_2432"), + QCase.case$.processIdentifier.eq("migration_test"), Pageable.ofSize(10) ).content @@ -104,11 +104,11 @@ class MigrationTest { "recreate_info_text": "" ]) useCase.dataSet["income"] = new DataField(1000) - }, "nae_2432", 2) + }, "migration_test", 2) } List<Case> casesAfterMigration = workflowService.search( - QCase.case$.processIdentifier.eq("nae_2432"), + QCase.case$.processIdentifier.eq("migration_test"), Pageable.ofSize(10) ).content @@ -130,7 +130,7 @@ class MigrationTest { @Test void migrationHelperShouldUpdatePetriNetAndApplyCustomTransitionRoleUpdate() { ProcessRole role = migrationHelper.createRoleInNet( - "nae_2432", + "migration_test", "migration_supervisor", "Migration supervisor" ) @@ -144,9 +144,9 @@ class MigrationTest { ] ) - migrationHelper.updateNetIgnoreRoles("nae_2432", "nae_2432_v2.xml", [updateTransitionRole]) + migrationHelper.updateNetIgnoreRoles("migration_test", "migration_test_v2.xml", [updateTransitionRole]) - PetriNet migratedNet = petriNetService.getNewestVersionByIdentifier("nae_2432") + PetriNet migratedNet = petriNetService.getNewestVersionByIdentifier("migration_test") assert migratedNet.dataSet.containsKey("income") assert migratedNet.dataSet.containsKey("recreate_info_text") @@ -168,7 +168,7 @@ class MigrationTest { @Test void migrationHelperShouldUpdateTasksAndAddRoleToExistingTasks() { ProcessRole role = migrationHelper.createRoleInNet( - "nae_2432", + "migration_test", "migration_task_role", "Migration task role" ) @@ -184,7 +184,7 @@ class MigrationTest { ) Page<Case> casePage = workflowService.search( - QCase.case$.processIdentifier.eq("nae_2432"), + QCase.case$.processIdentifier.eq("migration_test"), Pageable.ofSize(10) ) @@ -240,7 +240,7 @@ class MigrationTest { assert !migrationHelper.hasErrors() List<Case> cases = workflowService.search( - QCase.case$.processIdentifier.eq("nae_2432"), + QCase.case$.processIdentifier.eq("migration_test"), Pageable.ofSize(10) ).content @@ -267,9 +267,9 @@ class MigrationTest { @Test void updateNetIgnoreRolesShouldMigrateExistingNet() { - migrationHelper.updateNetIgnoreRoles("nae_2432", "nae_2432_v2.xml") + migrationHelper.updateNetIgnoreRoles("migration_test", "migration_test_v2.xml") - def net = petriNetService.getNewestVersionByIdentifier("nae_2432") + def net = petriNetService.getNewestVersionByIdentifier("migration_test") assert net.dataSet.containsKey("income") assert net.transitions.values().any { it.importId == "recreate_person" } diff --git a/src/test/resources/nae_2432_v1.xml b/src/test/resources/petriNets/migration_test_v1.xml similarity index 99% rename from src/test/resources/nae_2432_v1.xml rename to src/test/resources/petriNets/migration_test_v1.xml index fc98584acb4..86050cce482 100644 --- a/src/test/resources/nae_2432_v1.xml +++ b/src/test/resources/petriNets/migration_test_v1.xml @@ -1,5 +1,5 @@ <document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.com/petriflow.schema.xsd"> - <id>nae_2432</id> + <id>migration_test</id> <version>1.0.0</version> <initials>NAE</initials> <title>NAE-2432 diff --git a/src/test/resources/nae_2432_v2.xml b/src/test/resources/petriNets/migration_test_v2.xml similarity index 99% rename from src/test/resources/nae_2432_v2.xml rename to src/test/resources/petriNets/migration_test_v2.xml index b1cab7bc21f..435b3f26ca0 100644 --- a/src/test/resources/nae_2432_v2.xml +++ b/src/test/resources/petriNets/migration_test_v2.xml @@ -1,5 +1,5 @@ - nae_2432 + migration_test 1.0.0 NAE NAE-2432 diff --git a/src/test/resources/petriNets/nae_2432_v1.xml b/src/test/resources/petriNets/nae_2432_v1.xml deleted file mode 100644 index fc98584acb4..00000000000 --- a/src/test/resources/petriNets/nae_2432_v1.xml +++ /dev/null @@ -1,257 +0,0 @@ - - nae_2432 - 1.0.0 - NAE - NAE-2432 - mic - true - true - false - NAE-2432 - - delete_info_text - - <init><h1>Finish this task to delete person.</h1></init> - </data> - <data type="text"> - <id>first_name</id> - <title>First name - John - First name of person - - - last_name - Last name - Doe - Last name of person - - - note - Note - Example note - Notes about this person - - - reset_info_text - - <init><h1>Finish task to reset this person.</h1></init> - </data> - <transition> - <id>delete_person</id> - <x>816</x> - <y>176</y> - <label>Delete person</label> - <icon>delete</icon> - <dataGroup> - <id>delete_person_0</id> - <cols>4</cols> - <layout>grid</layout> - <dataRef> - <id>delete_info_text</id> - <logic> - <behavior>visible</behavior> - </logic> - <layout> - <x>0</x> - <y>0</y> - <rows>2</rows> - <cols>4</cols> - <template>material</template> - <appearance>outline</appearance> - </layout> - <component> - <name>htmltextarea</name> - </component> - </dataRef> - </dataGroup> - </transition> - <transition> - <id>person_info</id> - <x>528</x> - <y>176</y> - <label>Person info</label> - <icon>person</icon> - <dataGroup> - <id>t1_0</id> - <cols>4</cols> - <layout>grid</layout> - <dataRef> - <id>first_name</id> - <logic> - <behavior>editable</behavior> - <behavior>required</behavior> - <behavior>immediate</behavior> - </logic> - <layout> - <x>0</x> - <y>0</y> - <rows>1</rows> - <cols>2</cols> - <template>material</template> - <appearance>outline</appearance> - </layout> - </dataRef> - <dataRef> - <id>last_name</id> - <logic> - <behavior>editable</behavior> - <behavior>required</behavior> - <behavior>immediate</behavior> - </logic> - <layout> - <x>2</x> - <y>0</y> - <rows>1</rows> - <cols>2</cols> - <template>material</template> - <appearance>outline</appearance> - </layout> - </dataRef> - <dataRef> - <id>note</id> - <logic> - <behavior>editable</behavior> - </logic> - <layout> - <x>0</x> - <y>1</y> - <rows>2</rows> - <cols>4</cols> - <template>material</template> - <appearance>outline</appearance> - </layout> - <component> - <name>textarea</name> - </component> - </dataRef> - </dataGroup> - </transition> - <transition> - <id>reset_person</id> - <x>816</x> - <y>304</y> - <label>Reset person</label> - <icon>reset_tv</icon> - <dataGroup> - <id>reset_person_0</id> - <cols>4</cols> - <layout>grid</layout> - <dataRef> - <id>reset_info_text</id> - <logic> - <behavior>visible</behavior> - </logic> - <layout> - <x>0</x> - <y>0</y> - <rows>2</rows> - <cols>4</cols> - <template>material</template> - <appearance>outline</appearance> - </layout> - <component> - <name>htmltextarea</name> - </component> - </dataRef> - </dataGroup> - </transition> - <place> - <id>p1</id> - <x>336</x> - <y>176</y> - <tokens>1</tokens> - <static>false</static> - </place> - <place> - <id>p2</id> - <x>656</x> - <y>176</y> - <tokens>0</tokens> - <static>false</static> - </place> - <place> - <id>p3</id> - <x>656</x> - <y>304</y> - <tokens>0</tokens> - <static>false</static> - </place> - <place> - <id>p4</id> - <x>976</x> - <y>176</y> - <tokens>1</tokens> - <static>false</static> - </place> - <arc> - <id>a1</id> - <type>regular</type> - <sourceId>p1</sourceId> - <destinationId>person_info</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a2</id> - <type>regular</type> - <sourceId>person_info</sourceId> - <destinationId>p2</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a3</id> - <type>regular</type> - <sourceId>person_info</sourceId> - <destinationId>p3</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a4</id> - <type>regular</type> - <sourceId>p2</sourceId> - <destinationId>delete_person</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a5</id> - <type>regular</type> - <sourceId>p3</sourceId> - <destinationId>reset_person</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a6</id> - <type>regular</type> - <sourceId>delete_person</sourceId> - <destinationId>p4</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a7</id> - <type>regular</type> - <sourceId>reset_person</sourceId> - <destinationId>p1</destinationId> - <multiplicity>1</multiplicity> - <breakpoint> - <x>816</x> - <y>368</y> - </breakpoint> - <breakpoint> - <x>336</x> - <y>368</y> - </breakpoint> - </arc> - <arc> - <id>a8</id> - <type>regular</type> - <sourceId>p2</sourceId> - <destinationId>reset_person</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a9</id> - <type>regular</type> - <sourceId>p3</sourceId> - <destinationId>delete_person</destinationId> - <multiplicity>1</multiplicity> - </arc> -</document> \ No newline at end of file diff --git a/src/test/resources/petriNets/nae_2432_v2.xml b/src/test/resources/petriNets/nae_2432_v2.xml deleted file mode 100644 index b1cab7bc21f..00000000000 --- a/src/test/resources/petriNets/nae_2432_v2.xml +++ /dev/null @@ -1,388 +0,0 @@ -<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.com/petriflow.schema.xsd"> - <id>nae_2432</id> - <version>1.0.0</version> - <initials>NAE</initials> - <title>NAE-2432 - mic - true - true - false - NAE-2432 - - data_editor - Data editor - - - person_creator - Person creator - - - person_recreator - Person recreator - - - person_remover - Person remover - - - person_reseter - Person resetter - - - delete_info_text - - <init><h1>Finish this task to delete person.</h1></init> - </data> - <data type="text"> - <id>first_name</id> - <title>First name - John - First name of person - - - income - Income - Income of person - 1000 - - - last_name - Last name - Doe - Last name of person - - - note - Note - Example note - Notes about this person - - - recreate_info_text - - <init><h1>Finish this task to recreate person.</h1></init> - </data> - <data type="text"> - <id>reset_info_text</id> - <title/> - <init><h1>Finish task to reset this person.</h1></init> - </data> - <transition> - <id>delete_person</id> - <x>816</x> - <y>176</y> - <label>Delete person</label> - <icon>delete</icon> - <roleRef> - <id>person_remover</id> - <logic> - <view>true</view> - <perform>true</perform> - </logic> - </roleRef> - <dataGroup> - <id>delete_person_0</id> - <cols>4</cols> - <layout>grid</layout> - <dataRef> - <id>delete_info_text</id> - <logic> - <behavior>visible</behavior> - </logic> - <layout> - <x>0</x> - <y>0</y> - <rows>2</rows> - <cols>4</cols> - <template>material</template> - <appearance>outline</appearance> - </layout> - <component> - <name>htmltextarea</name> - </component> - </dataRef> - </dataGroup> - </transition> - <transition> - <id>person_info</id> - <x>528</x> - <y>176</y> - <label>Person info</label> - <icon>person</icon> - <roleRef> - <id>data_editor</id> - <logic> - <view>true</view> - <perform>true</perform> - </logic> - </roleRef> - <roleRef> - <id>person_creator</id> - <logic> - <view>true</view> - <perform>true</perform> - </logic> - </roleRef> - <dataGroup> - <id>t1_0</id> - <cols>4</cols> - <layout>grid</layout> - <dataRef> - <id>first_name</id> - <logic> - <behavior>editable</behavior> - <behavior>required</behavior> - <behavior>immediate</behavior> - </logic> - <layout> - <x>0</x> - <y>0</y> - <rows>1</rows> - <cols>2</cols> - <template>material</template> - <appearance>outline</appearance> - </layout> - </dataRef> - <dataRef> - <id>last_name</id> - <logic> - <behavior>editable</behavior> - <behavior>required</behavior> - <behavior>immediate</behavior> - </logic> - <layout> - <x>2</x> - <y>0</y> - <rows>1</rows> - <cols>2</cols> - <template>material</template> - <appearance>outline</appearance> - </layout> - </dataRef> - <dataRef> - <id>income</id> - <logic> - <behavior>editable</behavior> - </logic> - <layout> - <x>0</x> - <y>1</y> - <rows>1</rows> - <cols>4</cols> - <template>material</template> - <appearance>outline</appearance> - </layout> - <component> - <name>currency</name> - <property key="fractionSize">2</property> - </component> - </dataRef> - <dataRef> - <id>note</id> - <logic> - <behavior>editable</behavior> - </logic> - <layout> - <x>0</x> - <y>2</y> - <rows>2</rows> - <cols>4</cols> - <template>material</template> - <appearance>outline</appearance> - </layout> - <component> - <name>textarea</name> - </component> - </dataRef> - </dataGroup> - </transition> - <transition> - <id>recreate_person</id> - <x>976</x> - <y>48</y> - <label>Recreate person</label> - <icon>emergency</icon> - <roleRef> - <id>person_recreator</id> - <logic> - <view>true</view> - <perform>true</perform> - </logic> - </roleRef> - <dataGroup> - <id>recreate_person_0</id> - <cols>4</cols> - <layout>grid</layout> - <dataRef> - <id>recreate_info_text</id> - <logic> - <behavior>visible</behavior> - </logic> - <layout> - <x>0</x> - <y>0</y> - <rows>2</rows> - <cols>4</cols> - <template>material</template> - <appearance>outline</appearance> - </layout> - <component> - <name>htmltextarea</name> - </component> - </dataRef> - </dataGroup> - </transition> - <transition> - <id>reset_person</id> - <x>816</x> - <y>304</y> - <label>Reset person</label> - <icon>reset_tv</icon> - <roleRef> - <id>person_reseter</id> - <logic> - <view>true</view> - <perform>true</perform> - </logic> - </roleRef> - <dataGroup> - <id>reset_person_0</id> - <cols>4</cols> - <layout>grid</layout> - <dataRef> - <id>reset_info_text</id> - <logic> - <behavior>visible</behavior> - </logic> - <layout> - <x>0</x> - <y>0</y> - <rows>2</rows> - <cols>4</cols> - <template>material</template> - <appearance>outline</appearance> - </layout> - <component> - <name>htmltextarea</name> - </component> - </dataRef> - </dataGroup> - </transition> - <place> - <id>p1</id> - <x>336</x> - <y>176</y> - <tokens>1</tokens> - <static>false</static> - </place> - <place> - <id>p2</id> - <x>656</x> - <y>176</y> - <tokens>0</tokens> - <static>false</static> - </place> - <place> - <id>p3</id> - <x>656</x> - <y>304</y> - <tokens>0</tokens> - <static>false</static> - </place> - <place> - <id>p4</id> - <x>976</x> - <y>176</y> - <tokens>0</tokens> - <static>false</static> - </place> - <arc> - <id>a1</id> - <type>regular</type> - <sourceId>p1</sourceId> - <destinationId>person_info</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a10</id> - <type>regular</type> - <sourceId>p4</sourceId> - <destinationId>recreate_person</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a11</id> - <type>regular</type> - <sourceId>recreate_person</sourceId> - <destinationId>p1</destinationId> - <multiplicity>1</multiplicity> - <breakpoint> - <x>336</x> - <y>48</y> - </breakpoint> - </arc> - <arc> - <id>a2</id> - <type>regular</type> - <sourceId>person_info</sourceId> - <destinationId>p2</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a3</id> - <type>regular</type> - <sourceId>person_info</sourceId> - <destinationId>p3</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a4</id> - <type>regular</type> - <sourceId>p2</sourceId> - <destinationId>delete_person</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a5</id> - <type>regular</type> - <sourceId>p3</sourceId> - <destinationId>reset_person</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a6</id> - <type>regular</type> - <sourceId>delete_person</sourceId> - <destinationId>p4</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a7</id> - <type>regular</type> - <sourceId>reset_person</sourceId> - <destinationId>p1</destinationId> - <multiplicity>1</multiplicity> - <breakpoint> - <x>816</x> - <y>368</y> - </breakpoint> - <breakpoint> - <x>336</x> - <y>368</y> - </breakpoint> - </arc> - <arc> - <id>a8</id> - <type>regular</type> - <sourceId>p2</sourceId> - <destinationId>reset_person</destinationId> - <multiplicity>1</multiplicity> - </arc> - <arc> - <id>a9</id> - <type>regular</type> - <sourceId>p3</sourceId> - <destinationId>delete_person</destinationId> - <multiplicity>1</multiplicity> - </arc> -</document> \ No newline at end of file From 20a24d0a04eba05f36efef16e8cc5751d333a7d6 Mon Sep 17 00:00:00 2001 From: renczesstefan <renczes.stefan@gmail.com> Date: Wed, 3 Jun 2026 10:26:17 +0200 Subject: [PATCH 20/20] Update migration error storage from map to thread-safe list using `CopyOnWriteArrayList` for improved error tracking and reporting --- .../migration/helpers/AbstractMigrationHelper.groovy | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy index d116c7901cb..19fef072cb6 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/helpers/AbstractMigrationHelper.groovy @@ -50,10 +50,11 @@ abstract class AbstractMigrationHelper<T> { protected final MigrationProperties migrationProperties /** - * A thread-safe map that stores migration errors encountered during the migration process. - * The map is keyed by a string identifier (typically a document ID or migration step identifier) - * and contains a list of {@link MigrationError} objects representing all errors associated with that key. - * This structure allows for efficient error tracking and reporting during bulk migration operations. + * A thread-safe list of migration errors that occurred during the migration process. + * This list stores all errors encountered while processing documents, allowing the migration + * to continue execution while collecting errors for later review and reporting. + * The list uses {@link CopyOnWriteArrayList} to ensure thread-safety during concurrent + * migration operations. */ private final List<MigrationError> migrationErrors